lassl을 이용한 언어모델 사전학습 (Feat. T5, UL2)
- 공유 링크 만들기
- X
- 이메일
- 기타 앱
업데이트
[22/10/4] 모두의말뭉치 + alpha로 학습한 KoUL2 모델을 huggingface hub에 릴리즈했습니다!
----------------------------------------
이 포스트에서는 lassl 오픈소스를 사용해서 실제로 한국어 코퍼스를 활용한 사전학습 모델을 만드는 법을 다룹니다. 제가 참여해서 구현한 T5와 UL2의 구현 방식, 고민한 내용들을 공유하고자 합니다. Lassl 소스코드는 여기에서 확인하실 수 있습니다.
TFRC 프로그램 소개 및 신청하기
1. 다음 사이트에 가서 TRC 프로그램의 form을 작성하고 신청합니다.
https://sites.research.google/trc/about/
2. 며칠 지나면 다음과 같은 메일이 옵니다.
3. console.cloud.google.com에서 프로젝트를 생성한 뒤 그 프로젝트 ID를 메일 내 링크를 통해 입력하면 됩니다. 그러면 다음과 같은 승인 메일이 며칠 내에 옵니다.
4. 이제 TPU instance를 무료로 사용할 수 있습니다!
GCP TPU 인스턴스와 디스크 만들기
export GCP_DISK_NAME=lassl-disk export GCP_INSTANCE_NAME=lassl-tpu export GCP_PROJECT=<your_project_id> # i.e. fast-cascade-123456 export GCP_ZONE=europe-west4-a gcloud compute disks create $GCP_DISK_NAME \ --project=$GCP_PROJECT \ --type=pd-standard \ --size=500GB \ --zone=$GCP_ZONE gcloud alpha compute tpus tpu-vm create $GCP_INSTANCE_NAME \ --accelerator-type v3-8 \ --version tpu-vm-pt-1.10 \ --zone $GCP_ZONE \ --project $GCP_PROJECT \ --data-disk source="projects/${GCP_PROJECT}/zones/${GCP_ZONE}/disks/${GCP_DISK_NAME}"
3. 디스크를 생성한 경우 다음 명령어로 포맷 및 마운트할 수 있습니다.
# Connect tpu-vm node gcloud alpha compute tpus tpu-vm ssh $GCP_INSTANCE_NAME # Check that your disk is visible and get its name lsblk # Mount disk in the data folder sudo mkfs.ext4 -m 0 -E lazy_itable_init=0,lazy_journal_init=0,discard /dev/sdb sudo mkdir -p workspace sudo mount -o discard,defaults /dev/sdb workspace sudo chmod a+w workspace
export GCP_USER_NAME=<your_user_name> export GCP_INSTANCE_PORT=<tpu_vm_external_ip> rsync -avP -e "ssh -i ~/.ssh/google_compute_engine" corpora $GCP_USER_NAME@$GCP_INSTANCE_PORT:/home/$GCP_USER_NAME/workspace/ cd ~/workspace/lassl/ pip3 install .
export XRT_TPU_CONFIG="localservice;0;localhost:51011" python3 xla_spawn.py --num_cores 8 pretrain_language_model.py --config_path <config_path> # i.e. configs/train-plm-with-tpu-t5.yaml
Troubleshooting
- 학습 시 torch_xla 관련 이슈
- core dumped 뜨면서 프로세스 꺼진다면 torch==1.9.0 및 xla==1.9.0으로 버전 낮춰보기
sudo bash /var/scripts/docker-login.sh sudo docker rm libtpu || true
sudo docker create --name libtpu gcr.io/cloud-tpu-v2-images/libtpu:pytorch-1.9 "/bin/bash" sudo docker cp libtpu:libtpu.so /lib
sudo pip3 uninstall --yes torch torch_xla torchvision
sudo pip3 install torch==1.9.0
sudo pip3 install torchvision==0.10.0
sudo pip3 install https://storage.googleapis.com/tpu-pytorch/wheels/tpuvm/torch_xla-1.9-cp38-cp38-linux_x86_64.whl
- pretrain_language_model.py 실행 전에 xla 환경변수가 설정되지 않은 경우 에러 발생함
export XRT_TPU_CONFIG="localservice;0;localhost:51011"
- symbol을 못찾겠다고 뜰 때 : torch version을 맞추기(downgrade or upgrade)
- libtpu error는 torch_xla 버전과 현재 설치된 libtpu 버전이 호환되지 않아 생기는 이슈이므로 적절한 version을 설치해주면 됨. 참고
- distutils 이 version이 없다고 나오는 경우
pip install setuptools==59.5.0
사전 학습의 논리적 순서
lassl을 기준으로 사전학습이 진행되는 순서에 대해서 설명하면, 먼저 적절한 형태(sent_text)로 corpus를 수집하고 해당 corpus를 활용해서 tokenizer를 학습합니다. 만일 이미 학습된 tokenizer가 있다면 이 과정은 생략할 수 있습니다. 이어서 Processor를 활용해서 사전학습 방식(i.e. BERT, T5, ...)에 맞게 corpus를 tokenize한 후 적절한 사이즈로 잘라서 huggingface dataset 형태로 적재합니다. 대부분의 경우 사전학습에 사용할 데이터 샘플의 시퀀스 길이에서 특수토큰(</s>, <s>) 등의 개수를 뺀 길이로 전 처리를 해둡니다. 마지막으로 실제로 학습할 때는 이 데이터를 읽어들여서 Collator에서 해당 모델에 맞게 batch를 dynamic하게 재구성하게 됩니다. 예를 들어 BERT라면 MLM을 하기 위해 random token을 masking하고, next sentence prediction을 위해 [sep]을 사이에 두고 두 문장을 이어서 준비할 것입니다. 만약 T5라면 sentinel token을 활용하여 span corruption을 수행하여 input과 label을 만들 것입니다. lassl 내에서 실제로 파일이 실행되는 순서는 train_tokenizer.py -> serialize_corpora.py -> pretrain_language_model.py 입니다. 더 자세한 설명은 lassl Readme에 잘 나와 있으니 참고해 주시면 좋겠습니다.
T5 및 UL2 구현 방식
T5, UL2의 사전학습 방식
새로운 모델을 추가하기 위해 구현해야 할 부분
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | def compute_indv_chunk_size(target_length, noise_density, mean_span_length): '''pre-corruption token length approximation for T5 and UL2''' def _tokens_length_to_inputs_length_targets_length(tokens_length): ''' https://github.com/google-research/text-to-text-transfer-transformer/blob/c3be7cf1c20e5f6d83e6de99377b653a3a0bc44a/t5/data/preprocessors.py#L2648 ''' # setting mean_span_length to None means prefix-lm that masks last 25% tokens num_noise_tokens = int(round(tokens_length * noise_density)) num_nonnoise_tokens = tokens_length - num_noise_tokens num_noise_spans = 1 if mean_span_length is None else int(round(num_noise_tokens / mean_span_length)) # inputs contain all nonnoise tokens, sentinels for all noise spans # and one EOS token. return ( num_nonnoise_tokens + num_noise_spans + 1, num_noise_tokens + num_noise_spans + 1) tokens_length = target_length - 1 while (_tokens_length_to_inputs_length_targets_length(tokens_length + 1)[0] <= target_length): tokens_length += 1 inputs_length, targets_length = _tokens_length_to_inputs_length_targets_length (tokens_length) if mean_span_length is None: mean_span_length = targets_length - 2 return tokens_length, targets_length, mean_span_length |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | def random_spans_noise_mask(noise_density: float, mean_span_length : float, length : int) -> torch.BoolTensor: ''' pytorch-ported version of https://github.com/google-research/text-to-text-transfer-transformer/blob/bb545f19ec221e6203dd05505573fbc0c0a9001f/t5/data/preprocessors.py#L2901''' orig_len = length length = max(length, 2) # set minumum to 2 to avoid degeneracy num_noise_tokens = round(noise_density * length) num_noise_tokens = min(max(num_noise_tokens, 1), length-1) # set maximum to length-1 num_noise_spans = round(num_noise_tokens / mean_span_length) num_noise_spans = max(num_noise_spans, 1) # set minumum to 1 num_nonnoise_tokens = length - num_noise_tokens def _random_segmentation(num_items, num_segments): # affected by global seed bars = torch.arange(num_items-1) < num_segments-1 bars = bars[torch.randperm(bars.size(0))] bars = torch.cat((torch.tensor([0]), bars), dim=0) # to make segment 0 nonzero segment_id = torch.cumsum(bars, dim=0) segment_length = torch.zeros(num_segments, dtype=torch.long).scatter_add(0, segment_id, torch.ones_like(segment_id)) return segment_length noise_span_lengths = _random_segmentation(num_noise_tokens, num_noise_spans) nonnoise_span_lengths = _random_segmentation(num_nonnoise_tokens, num_noise_spans) interleaved_span_lengths = torch.stack((nonnoise_span_lengths, noise_span_lengths), dim=1).reshape(-1) span_starts = torch.cumsum(interleaved_span_lengths, dim=0)[:-1] span_start_indicator = torch.zeros(length).long().scatter(0, span_starts, torch.ones_like(span_starts)) span_num = torch.cumsum(span_start_indicator, dim=0) is_noise = span_num % 2 == 1 return is_noise[:orig_len] def noise_span_to_unique_sentinel(tokenizer, tokens, noise_mask, append_last_sentinel=False, denoiser_prefix : Optional[str] = None, first_extra_id : str = "<extra_id_0>") -> torch.LongTensor: ''' pytorch-ported version of https://github.com/google-research/text-to-text-transfer-transformer/blob/bb545f19ec221e6203dd05505573fbc0c0a9001f/t5/data/preprocessors.py#L3074''' if not isinstance(tokens, torch.Tensor): tokens = torch.tensor(tokens) # sample consecutive substring from tokens if len(tokens) > len(noise_mask) # in case of T5, these two should match. In case of UL2, due to use of several denoisers, number of tokens could be larger than length of noise masks. if len(tokens) > len(noise_mask): offset = len(tokens) - len(noise_mask) random.seed(tokens[0].item()) # seed that makes same example to match in both making inputs and targets start_idx = random.randint(0,offset) tokens = tokens[start_idx : start_idx + len(noise_mask)] assert len(tokens) == len(noise_mask) prev_token_is_noise = torch.cat((torch.tensor([0]), noise_mask[:-1]), dim=0).bool() first_noise_tokens = torch.logical_and( noise_mask, torch.logical_not(prev_token_is_noise)) subsequent_noise_tokens = torch.logical_and(noise_mask, prev_token_is_noise) sentinel = tokenizer.get_vocab()[first_extra_id] + 1 - torch.cumsum(first_noise_tokens.long(), dim=0) tokens = torch.where(first_noise_tokens, sentinel, tokens) ret = torch.masked_select(tokens, torch.logical_not(subsequent_noise_tokens)) if append_last_sentinel: # target masking needs additional sentinel token at last position last_sentinel_id = sentinel.min().reshape(-1) - 1 ret = torch.cat((ret, last_sentinel_id), dim=0) ret = torch.cat((ret, torch.tensor([tokenizer.eos_token_id], dtype=torch.long)), dim=0) # add eos token if denoiser_prefix: # used only for UL2, which prepends one of [S2S], [NLG], [NLU] during training. # These tokens are not treated as special tokens but they are tokenized as normal tokens. denoiser_prefix_enc = torch.tensor(tokenizer.encode(denoiser_prefix)[:1], dtype=torch.long) ret = torch.cat((denoiser_prefix_enc, ret), dim=0) return ret |
댓글
댓글 쓰기