이번 포스팅은 앞서 개발한 KoBART를 활용한 카카오톡 대화 요약 서비스를 보완한 것이다. 가장 핵심적으로는. ipynb 확장자로 구성된 파일을. py 확장자 python script로 구현한 것이다. 물론 그 외에도 여러 가지 개선을 하였다. 개선사항은 다음과 같다.
기존 서비스 | 개선 서비스 | 진행사항(%) |
---|---|---|
.ipynb로 구현 | .py로 현업에 맞게 재구현 | 100% |
post-train 모든 토큰 loss 계산 | masking만 loss 계산 | 100% |
post-train meta data 미적용 | post-train meta data 적용 | 100%(불필요) |
잘못된 비율로 적용된 masking | masking 적용 개선 | 100%(불필요) |
잘못된 datacollator 사용 | 직접 datacollator 구현 | 100% |
MLM 기법의 경우는 앞의 모델과 평가편 포스팅을 다시 재수정해서 올리기는 하였지만 여기서도 한 번 더 자세히 다루어 보겠다.
python scirpt로 개선하기 위하여 post_dataset.py와 post_train.py 파일 두개로 구현하였다. 우선 post-dataset.py부터 살펴보도록 하겠다.
post_dataset.py 다음과 같이 7가지의 함수로 구현되어 있다.
이 함수들은 post_train.py에서 컨트롤 하여 사용할 예정이다.
fine-tuning의 Python Script는 post-train과 크게 다르지 않다. 달라진 점을 다음과 같이 정리보았다.
우선 가장 핵심이 되는 MLM 기법을 적용한 코드에 대해서 알아보도록 하겠다. 아래의 코드는 masking_rate = 0.15와 inputs의 len을 곱하여 해당 input에 몇 개의 maskig을 해야 할지 지정한다. 그리고 random.sample()을 통하여 masking 해야 할 개수만큼 랜덤으로 지정하여 position을 저장한다. 이를 통해서 position된 부분에는 masking 하고 나머지는 토큰 값을 그대로 입력하는 것이다. 다음은 label에 손실 함수를 계산할 토큰을 지정하기 위한 코드를 소개하도록 하겠다.
mask_num = int(len(inputs)*config.masking_rate)
mask_positions = random.sample([x for x in range(len(inputs))], mask_num)
corrupt_token = []
for pos in range(len(inputs)):
if pos in mask_positions:
corrupt_token.append(tokenizer.mask_token_id)
else:
corrupt_token.append(inputs[pos])
우선 MLM 기법을 적용한 tokenize 및 padding 처리된 input 값을 가져와 pad token 부분은 제거하여 label과 비교하여 값을 입력할 수 있도록 만들어 준다. 그리고 masking 되지 않은 값들의 위치를 가져와 해당 위치에는 -100을 입력한다.
none_mask = []
corrupt_token = [x for x in corrupt_token if x != tokenizer.pad_token_id]
for i in range(len(corrupt_token)):
if corrupt_token[i] != tokenizer.mask_token_id:
none_mask.append(i)
for mask_num in none_mask:
inputs[mask_num] = config.ignore_index
다음과 같이 각 문장 맨 앞에 meta data가 되는 topic을 #으로 감싸서 입력하여 준다. 이는 나중에 special token을 지정하여 학습에 유용한 정보를 가져다줄 것이다.
topic_list.append(datum['header']['dialogueInfo']['topic'])
.
.
.
total_list[i].insert(0,"#"+topic_list[i]+"#")
Datacollator는 여러 가지 종류가 있다. BART와 같이 Encoder & Decoder로 이루어진 모델은 DataCollatorForSeq2seq를 사용하면 된다. MLM을 적용하는 경우에는 DataCollatorForLanguageModeling를 사용한다. 하지만 seq2seq이기 때문에 DataCollatorForLanguageModeling를 사용하지 못한다. 그리고 이번 프로젝트에서는 DataCollatorForSeq2seq의 역할인 seq2seq모델에서 학습이 가능하도록 배치를 준비할 때 레이블을 한 스텝 오른쪽으로 이동시켜 디코더 입력을 만드는 것을 직접 구현할 것이다.
허깅페이스에서 제공해 주는 DataCollatorForSeq2seq의 코드이다. 이것과 비교하여 다음 직접 구현한 코드를 살펴보도록 하겠다.
class DataCollatorForSeq2Seq:
tokenizer: PreTrainedTokenizerBase
model: Optional[Any] = None
padding: Union[bool, str, PaddingStrategy] = True
max_length: Optional[int] = None
pad_to_multiple_of: Optional[int] = None
label_pad_token_id: int = -100
return_tensors: str = "pt"
def __call__(self, features, return_tensors=None):
if return_tensors is None:
return_tensors = self.return_tensors
labels = [feature["labels"] for feature in features] if "labels" in features[0].keys() else None
# We have to pad the labels before calling `tokenizer.pad` as this method won't pad them and needs them of the
# same length to return tensors.
if labels is not None:
max_label_length = max(len(l) for l in labels)
if self.pad_to_multiple_of is not None:
max_label_length = (
(max_label_length + self.pad_to_multiple_of - 1)
// self.pad_to_multiple_of
* self.pad_to_multiple_of
)
padding_side = self.tokenizer.padding_side
for feature in features:
remainder = [self.label_pad_token_id] * (max_label_length - len(feature["labels"]))
if isinstance(feature["labels"], list):
feature["labels"] = (
feature["labels"] + remainder if padding_side == "right" else remainder + feature["labels"]
)
elif padding_side == "right":
feature["labels"] = np.concatenate([feature["labels"], remainder]).astype(np.int64)
else:
feature["labels"] = np.concatenate([remainder, feature["labels"]]).astype(np.int64)
features = self.tokenizer.pad(
features,
padding=self.padding,
max_length=self.max_length,
pad_to_multiple_of=self.pad_to_multiple_of,
return_tensors=return_tensors,
)
# prepare decoder_input_ids
if (
labels is not None
and self.model is not None
and hasattr(self.model, "prepare_decoder_input_ids_from_labels")
):
decoder_input_ids = self.model.prepare_decoder_input_ids_from_labels(labels=features["labels"])
features["decoder_input_ids"] = decoder_input_ids
return features
여기에서 핵심은 label을 구현하는 코드이다. 특히 add_ignored_data() 함수를 유의깊게 보면 될 것 같다. 이를 통해서 DataCollatorForSeq2seq의 형식으로 이번 모델에 맞게 구현할 수 있었다.
def add_ignored_data(inputs, config, corrupt_token, tokenizer):
none_mask = []
corrupt_token = [x for x in corrupt_token if x != tokenizer.pad_token_id]
for i in range(len(corrupt_token)):
if corrupt_token[i] != tokenizer.mask_token_id:
none_mask.append(i)
for mask_num in none_mask:
inputs[mask_num] = config.ignore_index
if len(inputs)+1 < config.max_len:
pad = [config.ignore_index] * (config.max_len - (len(inputs)+1)) # ignore_index즉 -100으로 패딩을 만들 것인데 max_len - lne(inpu)
inputs = np.concatenate([inputs, [tokenizer.eos_token_id], pad])
else:
inputs = inputs + [tokenizer.eos_token_id]
inputs = inputs[:config.max_len]
return inputs
def add_padding_data(inputs, config, tokenizer, is_mlm=False):
if is_mlm:
mask_num = int(len(inputs)*config.masking_rate)
mask_positions = random.sample([x for x in range(len(inputs))], mask_num)
corrupt_token = []
for pos in range(len(inputs)):
if pos in mask_positions:
corrupt_token.append(tokenizer.mask_token_id)
else:
corrupt_token.append(inputs[pos])
if len(corrupt_token) < config.max_len:
pad = [tokenizer.pad_token_id] * (config.max_len - len(corrupt_token))
inputs = np.concatenate([corrupt_token, pad])
else:
inputs = corrupt_token[:config.max_len]
else:
if len(inputs) < config.max_len:
pad = [tokenizer.pad_token_id] * (config.max_len - len(inputs))
inputs = np.concatenate([inputs, pad])
else:
inputs = inputs[:config.max_len]
return inputs
def preprocess_data(data_to_process, tokenizer, config):
label_id= []
label_ids = []
dec_input_ids = []
input_ids = []
for i in range(len(data_to_process['Text'])):
input_ids.append(add_padding_data(tokenizer.encode(data_to_process['Text'][i], add_special_tokens=False), config, tokenizer, is_mlm=True))
label_id.append(tokenizer.encode(data_to_process['Text'][i]))
dec_input_id = tokenizer('<s>')['input_ids']
dec_input_id += label_id[i]
dec_input_ids.append(add_padding_data(dec_input_id, config, tokenizer))
label_ids.append(add_ignored_data(label_id[i], config, input_ids[i], tokenizer))
return {'input_ids': torch.tensor(input_ids),
'attention_mask' : torch.tensor((np.array(input_ids) != tokenizer.pad_token_id).astype(int)),
'decoder_input_ids': torch.tensor(dec_input_ids),
'decoder_attention_mask': torch.tensor((np.array(dec_input_ids) != tokenizer.pad_token_id).astype(int)),
'labels': torch.tensor(label_ids)}
마지막으로 수정된 코드를 검증하기 위해서 학습을 돌렸는데 성능이 이전보다 더 떨어졌다. 그래서 여러 가설을 세우고 테스트를 해보았다.
이 결과 길이당 0.15비율로 masking 한 것보다 무작위로 생성된 tensor값의 0.15미만 값이 성능이 더 높았다. 실수로 한 코드였지만 성능은 더 좋았던 것이다. masking된 토큰을 확인해본 결과 이유는 더 많은 token이 masking되기 때문으로 확인된다. 그래서 0.15~0.3%로 masking 하면 될 것이다. 이 이상의 실험은 colab 컴퓨팅이 부족하여 더 진행하지 못했다.
그리고 사실 1월에 post-train시 meta data 사용시 성능 변화에 대해서 실험했었지만 기록을 제대로 안 해서 잊고 meta data를 적용했다. meta data를 적용하면 성능이 떨어지는 이유는 MLM기법으로 구어체 대화문에 대한 모델의 적응을 하는 중인데 메타 데이터가 들어가면 masking된 '-거다' 등의 token을 예측하는데에 대한 유용하지 않은 값을 줄 수 있기 때문이다.
이로써 추가로 개선한 사항들을 마무리하고 KoBART를 활용한 카카오톡 대화 요약 서비스를 마무리하고자 한다. 물론 시간이 지나 더 실력이 쌓였을 때 지금의 코드를 보고 개선해야 할 점이 보인다면 다시 개선해서 포스팅할 예정이다.
github로 이동 : KoBART를 활용한 카카오톡 대화 요약 서비스