본문 바로가기

프로젝트/CausaLM (NLP)

CausaLM: Causal Model Explanation ThroughCounterfactual Language Models (2022) - CausaLM 프로젝트 (5) Final

728x90
반응형
 CausaLM 프로젝트 (5)

해당 프로젝트의 논문 내용 및 github 관련 자료들은 아래 글을 참고하시면 되겠습니다.

2023.09.18 - [프로젝트/CausaLM (NLP)] - CausaLM: Causal Model Explanation ThroughCounterfactual Language Models (2022) 논문 리뷰 - CausaLM 프로젝트 (1)

 

CausaLM: Causal Model Explanation ThroughCounterfactual Language Models (2022) 논문 리뷰 - CausaLM 프로젝트 (1)

CausaLM 프로젝트 (1) KUBIG 학회에서 방학 분반이 끝나고, 해당 분야의 advanced project를 하나 선정하여 진행했다. NLP 분반의 스터디를 진행했기 때문에, Language model에 관심을 가지고 있었는데 부원 한

tjddms9376.tistory.com

2023.09.26 - [프로젝트/CausaLM (NLP)] - CausaLM: Causal Model Explanation ThroughCounterfactual Language Models (2022) 논문 리뷰 - CausaLM 프로젝트 (2)

 

CausaLM: Causal Model Explanation ThroughCounterfactual Language Models (2022) 논문 리뷰 - CausaLM 프로젝트 (2)

CausaLM 프로젝트 (2) 지난 포스팅에서 논문의 본격적인 내용에 들어가기 전까지의 내용들을 살펴보았다. 2023.09.18 - [학부/논문 리뷰] - CausaLM: Causal Model Explanation ThroughCounterfactual Language Models (2022)

tjddms9376.tistory.com

2023.10.04 - [프로젝트/CausaLM (NLP)] - CausaLM: Causal Model Explanation ThroughCounterfactual Language Models (2022) 논문 리뷰 - CausaLM 프로젝트 (3)

 

CausaLM: Causal Model Explanation ThroughCounterfactual Language Models (2022) 논문 리뷰 - CausaLM 프로젝트 (3)

CausaLM 프로젝트 (3) 2023.09.18 - [학부/논문 리뷰] - CausaLM: Causal Model Explanation ThroughCounterfactual Language Models (2022) 논문 리뷰 - CausaLM 프로젝트 (1) 2023.09.26 - [분류 전체보기] - CausaLM: Causal Model Explanation Th

tjddms9376.tistory.com

2023.10.20 - [분류 전체보기] - CausaLM: Causal Model Explanation ThroughCounterfactual Language Models (2022) - CausaLM 프로젝트 (4)

 

CausaLM: Causal Model Explanation ThroughCounterfactual Language Models (2022) - CausaLM 프로젝트 (4)

CausaLM 프로젝트 (4) 해당 논문에 대한 포스팅은 아래 글들을 참고해주시면 됩니다. 2023.09.18 - [프로젝트/CausaLM (NLP)] - CausaLM: Causal Model Explanation ThroughCounterfactual Language Models (2022) 논문 리뷰 - CausaLM

tjddms9376.tistory.com

 


해당 프로젝트의 GitHub 주소


 이 github는 저희 팀이 작성한 repository로, readME 작성은 제가 맡았습니다.

자유롭게 보시고 피드백해주시면 감사하겠습니다!

https://github.com/KU-BIG/KUBIG_2023_FALL/tree/main/4.%20KUBIG%20CONTEST/NLP/CausaLM


 

프로젝트에 필요한 논문 리뷰를 마치고, github에 올라온 코드들을 바탕으로 GPU 서버를 이용하여 프로젝트를 진행했다.

 


서버 환경 구축


이 프로젝트는 3인 1팀으로 진행하기로 예정되어 있었으나, 팀원 중 한분이 부득이한 사정으로 2인 1팀으로 진행하게 되었다. 그래서 많은 부분들을 신경 써야 했고, GPU 서버를 본격적으로 처음 다뤄봤기에 환경에 연결하는 것도 힘들었다.

팀장님이 대부분의 환경을 구축해주셨고, 나는 visual studio code를 통해 SSH 연결 + mobaXterm을 이용한 서버 폴더 관리를 맡았다.

원격 서버 연결하는 방법은 아래 링크를 참고했다.

https://seokhyun2.tistory.com/42

 

vscode를 활용하여 원격 서버에 연결하기

회사에서 서버를 사용할 때, putty를 활용하는데 IDE를 사용하고 싶다는 생각이 문득 들어서 찾아보니 vscode는 community 버전을 활용해도 서버랑 원격 접속을 할 수 있더군요! 그래서 오늘은 윈도우

seokhyun2.tistory.com

GPU 서버는 학회 측에서 제공해준 서버를 사용했으며 2인이었기에 2개의 GPU를 사용할 수 있었다. 해당 task들을 진행할 때, dataparallel 문제로 많이 애를 먹었었는데, 이는 추후에 다루기로 하고 결론적으로 각자 1개씩 쓰느라 dataparallel을 사용하진 않았다.

 


Pre-requisite


conda 가상환경을 만들어 SSH 원격 접속 후, interpreter 설정을 통해 해당 가상환경으로 돌아갈 수 있게 설정을 했다.

필요한 package들은 논문에 기재된 Github에 잘 설명되어 있어서 큰 문제가 없었다.

우리가 작성한 github에 comment를 달아두었지만, 한번 더 설명하자면 causalm_gpu_env.yml 파일 안에 필요한 환경 설정들이 다 포함되어 있어서 저 명령어만 실행해주면 크게 문제가 없다.

아래와 같은 causalm 환경을 구축하고 진행했다.

이 때, 내가 가장 헷갈렸던 부분들이 있었는데 내가 구축한 환경은 causalm 환경이니까 저기를 interpreter로 설정하고 진행해야겠지 ? 였다.

하지만 저긴 작업 영역일 뿐, base를 interpreter로 설정해야 conda 환경에서 구축한 작업 영역 가상환경이 정상적으로 작동할 수 있었다. 이 부분은 나중에 작업할 때도 유의하면서 진행해야 할 것 같다.

 

또한, AI 분야에서는 조금 오래된 모델을 그대로 구축하려다 보니 version 문제가 있었다.

pytorch_lightning의 버전이 가장 큰 문제였는데, 0.5.3.2 ver로 다운 받고 사용해도 해결되지 않았다.

알고 보니, dataloader 부분에서 기존 코드에 나와있던 부분들이 현재는 지원하지 않았기 때문에 torch의 dataloader 부분으로 대체해서 해결했다.

또한,  Data parallel때문에 GPU가 2개가 사용돼서 데이터의 입력 형식이 바뀌어 계속 오류가 발생하기도 했다. 

이 부분은 앞서 말한대로, 각자 1개씩 GPU를 쓰기로 하고 n_gpu > 1 일 때, 2개를 쓸 수 있도록 하는 명령어를 수정해서 해결했다.


프로젝트 진행 - Task 선정


우리가 선정한 Task는 해당 논문에서 다룬 task중 1개인  'Adjective_sentiment' 부분을 reproducing & 새로운 모델로 task 수행하는 것이었다.

  • 새로운 모델로 바꾸는 건, 기존에는 BERT-cased 모델을 사용했었는데 우리 같은 경우, BERT-large 모델로도 진행해보는 것이었다.
  • Reproducing은 code study를 통해 논문에서 진행한 과정 그대로 재현해보는 것이 목적이었으며, 이 과정에서 dataset 구축 및 편집 등의 다양한 과정을 거쳐야만 했다.
    • 3개의 데이터셋으로 각각의 모델을 사전 훈련 시킨 후, balanced data로 test한 결과를 보는 것이 목적이다.
    • Aggressive로 훈련되면 balanced data에 대해서 정확한 예측이 안 될수 있는데, 논문의 결과에 따르면 bias가 담긴 data로 학습을 해도 성능의 차이가 유의미하지 않다. 즉, debiasing 효과가 있는 것을 증명하기 위한 task라고 생각하면 된다.


프로젝트 진행 - Dataset 구축


논문에서 다룬 내용처럼, dataset은 크게 3가지의 버전으로 나뉜다.

  • Aggressive
  • Gentle
  • Balanced

3개의 version으로 dataset을 새로 구축하는 code를 돌려야 했으며, 이 부분은 팀장님이 맡아서 진행해주셨다.


프로젝트 진행 - MLM_task


3개의 version data를 csv 파일로 전달 받아서, 이를 바탕으로 MLM task부터 진행했다.

위와 같이 SSH 접속 후, 경로 설정과 같은 기본적인 세팅만 바꾸고 진행했다.

# package 및 logger 설정
from pathlib import Path
import torch
import logging
import json
import random
import numpy as np
import pandas as pd
from collections import namedtuple, defaultdict
from tempfile import TemporaryDirectory

from torch.utils.data import DataLoader, Dataset, RandomSampler
from torch.utils.data.distributed import DistributedSampler
from tqdm import tqdm

from transformers.tokenization_bert import BertTokenizer
from transformers.optimization import AdamW, get_linear_schedule_with_warmup
from BERT.lm_finetune.bert_mlm_finetune import BertForMLMPreTraining
from Sentiment_Adjectives.lm_finetune.pregenerate_training_data import EPOCHS
from BERT.bert_text_dataset import BertTextDataset
from utils import init_logger
from constants import RANDOM_SEED, BERT_PRETRAINED_MODEL, NUM_CPU, \
    SENTIMENT_ADJECTIVES_PRETRAIN_DATA_DIR, SENTIMENT_ADJECTIVES_PRETRAIN_MLM_DIR

BATCH_SIZE = 10
FP16 = False

InputFeatures = namedtuple("InputFeatures", "input_ids input_mask lm_label_ids")
logger = init_logger("MLM-pretraining", f"{SENTIMENT_ADJECTIVES_PRETRAIN_MLM_DIR}")

필요한 package들을 먼저 import하고 logger 설정을 통해 안되는 부분들이 있을 경우, log message로 문제점들을 찾아갔다.

def convert_example_to_features(example, tokenizer, max_seq_length):
    tokens = example["tokens"]
    masked_lm_positions = np.array([int(i) for i in example["masked_lm_positions"]])
    masked_lm_labels = example["masked_lm_labels"]

    # assert len(tokens) == len(segment_ids) <= max_seq_length  # The preprocessed data should be already truncated
    input_ids = tokenizer.convert_tokens_to_ids(tokens)
    masked_label_ids = tokenizer.convert_tokens_to_ids(masked_lm_labels)

    input_array = np.zeros(max_seq_length, dtype=int)
    input_array[:len(input_ids)] = input_ids

    mask_array = np.zeros(max_seq_length, dtype=bool)
    mask_array[:len(input_ids)] = 1

    lm_label_array = np.full(max_seq_length, dtype=int, fill_value=BertTextDataset.MLM_IGNORE_LABEL_IDX)
    lm_label_array[masked_lm_positions] = masked_label_ids

    features = InputFeatures(input_ids=input_array,
                             input_mask=mask_array,
                             lm_label_ids=lm_label_array)
    return features

class PregeneratedDataset(Dataset):
    def __init__(self, training_path, epoch, tokenizer, num_data_epochs, reduce_memory=False):
        self.vocab = tokenizer.vocab
        self.tokenizer = tokenizer
        self.epoch = epoch
        self.data_epoch = epoch % num_data_epochs
        data_file = training_path / f"{BERT_PRETRAINED_MODEL}_epoch_{self.data_epoch}.json"
        metrics_file = training_path / f"{BERT_PRETRAINED_MODEL}_epoch_{self.data_epoch}_metrics.json"
        assert data_file.is_file() and metrics_file.is_file()
        metrics = json.loads(metrics_file.read_text())
        num_samples = metrics['num_training_examples']
        seq_len = metrics['max_seq_len']
        self.temp_dir = None
        self.working_dir = None
        if reduce_memory:
            self.temp_dir = TemporaryDirectory()
            self.working_dir = Path(self.temp_dir.name)
            input_ids = np.memmap(filename=self.working_dir/'input_ids.memmap',
                                  mode='w+', dtype=np.int32, shape=(num_samples, seq_len))
            input_masks = np.memmap(filename=self.working_dir/'input_masks.memmap',
                                    shape=(num_samples, seq_len), mode='w+', dtype=np.bool)
            lm_label_ids = np.memmap(filename=self.working_dir/'lm_label_ids.memmap',
                                     shape=(num_samples, seq_len), mode='w+', dtype=np.int32)
            lm_label_ids[:] = BertTextDataset.MLM_IGNORE_LABEL_IDX
        else:
            input_ids = np.zeros(shape=(num_samples, seq_len), dtype=np.int32)
            input_masks = np.zeros(shape=(num_samples, seq_len), dtype=np.bool)
            lm_label_ids = np.full(shape=(num_samples, seq_len), dtype=np.int32, fill_value=BertTextDataset.MLM_IGNORE_LABEL_IDX)
        logging.info(f"Loading training examples for epoch {epoch}")
        with data_file.open() as f:
            for i, line in enumerate(tqdm(f, total=num_samples, desc="Training examples")):
                line = line.strip()
                example = json.loads(line)
                features = convert_example_to_features(example, tokenizer, seq_len)
                input_ids[i] = features.input_ids
                input_masks[i] = features.input_mask
                lm_label_ids[i] = features.lm_label_ids
        assert i == num_samples - 1  # Assert that the sample count metric was true
        logging.info("Loading complete!")
        self.num_samples = num_samples
        self.seq_len = seq_len
        self.input_ids = input_ids
        self.input_masks = input_masks
        self.lm_label_ids = lm_label_ids

    def __len__(self):
        return self.num_samples

    def __getitem__(self, item):
        return (torch.tensor(self.input_ids[item].astype(np.int64)),
                torch.tensor(self.input_masks[item].astype(np.int64)),
                torch.tensor(self.lm_label_ids[item].astype(np.int64)))
   def pretrain_on_treatment(args):
    assert args.pregenerated_data.is_dir(), \
        "--pregenerated_data should point to the folder of files made by pregenerate_training_data.py!"

    samples_per_epoch = []
    for i in range(args.epochs):
        epoch_file = args.pregenerated_data / f"{BERT_PRETRAINED_MODEL}_epoch_{i}.json"
        metrics_file = args.pregenerated_data / f"{BERT_PRETRAINED_MODEL}_epoch_{i}_metrics.json"
        if epoch_file.is_file() and metrics_file.is_file():
            metrics = json.loads(metrics_file.read_text())
            samples_per_epoch.append(metrics['num_training_examples'])
        else:
            if i == 0:
                exit("No training data was found!")
            print(f"Warning! There are fewer epochs of pregenerated data ({i}) than training epochs ({args.epochs}).")
            print("This script will loop over the available data, but training diversity may be negatively impacted.")
            num_data_epochs = i
            break
    else:
        num_data_epochs = args.epochs

    if args.local_rank == -1 or args.no_cuda:
        device = torch.device("cuda" if torch.cuda.is_available() and not args.no_cuda else "cpu")
        n_gpu = torch.cuda.device_count()
    else:
        torch.cuda.set_device(args.local_rank)
        device = torch.device("cuda", args.local_rank)
        n_gpu = 1
        # Initializes the distributed backend which will take care of sychronizing nodes/GPUs
        torch.distributed.init_process_group(backend='nccl')
    logging.info("device: {} n_gpu: {}, distributed training: {}, 16-bits training: {}".format(
        device, n_gpu, bool(args.local_rank != -1), args.fp16))

    if args.gradient_accumulation_steps < 1:
        raise ValueError("Invalid gradient_accumulation_steps parameter: {}, should be >= 1".format(
                            args.gradient_accumulation_steps))

    args.train_batch_size = args.train_batch_size // args.gradient_accumulation_steps

    random.seed(args.seed)
    np.random.seed(args.seed)
    torch.manual_seed(args.seed)
    if n_gpu > 0:
        torch.cuda.manual_seed_all(args.seed)

    if args.output_dir.is_dir() and list(args.output_dir.iterdir()):
        logging.warning(f"Output directory ({args.output_dir}) already exists and is not empty!")
    args.output_dir.mkdir(parents=True, exist_ok=True)

    tokenizer = BertTokenizer.from_pretrained(args.bert_model, do_lower_case=args.do_lower_case)

    total_train_examples = 0
    for i in range(args.epochs):
        # The modulo takes into account the fact that we may loop over limited epochs of data
        total_train_examples += samples_per_epoch[i % len(samples_per_epoch)]

    num_train_optimization_steps = int(
        total_train_examples / args.train_batch_size / args.gradient_accumulation_steps)
    if args.local_rank != -1:
        num_train_optimization_steps = num_train_optimization_steps // torch.distributed.get_world_size()

그 후, 우리가 구축한 dataset을 learning process에 넣기 위해 새로운 형식으로 구축을 해야했다. 우리가 가지고 있는 json 파일들을 이용하여 데이터를 불러오고, 필요한 matrics도 불러온 뒤에 우리가 필요한 형식으로 새롭게 구축하는 함수를 정의했다.

def pretrain_on_treatment(args):
    assert args.pregenerated_data.is_dir(), \
        "--pregenerated_data should point to the folder of files made by pregenerate_training_data.py!"

    samples_per_epoch = []
    for i in range(args.epochs):
        epoch_file = args.pregenerated_data / f"{BERT_PRETRAINED_MODEL}_epoch_{i}.json"
        metrics_file = args.pregenerated_data / f"{BERT_PRETRAINED_MODEL}_epoch_{i}_metrics.json"
        if epoch_file.is_file() and metrics_file.is_file():
            metrics = json.loads(metrics_file.read_text())
            samples_per_epoch.append(metrics['num_training_examples'])
        else:
            if i == 0:
                exit("No training data was found!")
            print(f"Warning! There are fewer epochs of pregenerated data ({i}) than training epochs ({args.epochs}).")
            print("This script will loop over the available data, but training diversity may be negatively impacted.")
            num_data_epochs = i
            break
    else:
        num_data_epochs = args.epochs

    if args.local_rank == -1 or args.no_cuda:
        device = torch.device("cuda" if torch.cuda.is_available() and not args.no_cuda else "cpu")
        n_gpu = torch.cuda.device_count()
    else:
        torch.cuda.set_device(args.local_rank)
        device = torch.device("cuda", args.local_rank)
        n_gpu = 1
        # Initializes the distributed backend which will take care of sychronizing nodes/GPUs
        torch.distributed.init_process_group(backend='nccl')
    logging.info("device: {} n_gpu: {}, distributed training: {}, 16-bits training: {}".format(
        device, n_gpu, bool(args.local_rank != -1), args.fp16))

    if args.gradient_accumulation_steps < 1:
        raise ValueError("Invalid gradient_accumulation_steps parameter: {}, should be >= 1".format(
                            args.gradient_accumulation_steps))

    args.train_batch_size = args.train_batch_size // args.gradient_accumulation_steps

    random.seed(args.seed)
    np.random.seed(args.seed)
    torch.manual_seed(args.seed)
    if n_gpu > 0:
        torch.cuda.manual_seed_all(args.seed)

    if args.output_dir.is_dir() and list(args.output_dir.iterdir()):
        logging.warning(f"Output directory ({args.output_dir}) already exists and is not empty!")
    args.output_dir.mkdir(parents=True, exist_ok=True)

    tokenizer = BertTokenizer.from_pretrained(args.bert_model, do_lower_case=args.do_lower_case)

    total_train_examples = 0
    for i in range(args.epochs):
        # The modulo takes into account the fact that we may loop over limited epochs of data
        total_train_examples += samples_per_epoch[i % len(samples_per_epoch)]

    num_train_optimization_steps = int(
        total_train_examples / args.train_batch_size / args.gradient_accumulation_steps)
    if args.local_rank != -1:
        num_train_optimization_steps = num_train_optimization_steps // torch.distributed.get_world_size()

    # Prepare model
    model = BertForMLMPreTraining.from_pretrained(args.bert_model)
    if args.fp16:
        model.half()
    model.to(device)
    if args.local_rank != -1:
        try:
            from apex.parallel import DistributedDataParallel as DDP
        except ImportError:
            raise ImportError(
                "Please install apex from https://www.github.com/nvidia/apex to use distributed and fp16 training.")
        model = DDP(model)
    #elif n_gpu > 1:
    #    model = torch.nn.DataParallel(model)

    # Prepare optimizer
    param_optimizer = list(model.named_parameters())
    no_decay = ['bias', 'LayerNorm.bias', 'LayerNorm.weight']
    optimizer_grouped_parameters = [
        {'params': [p for n, p in param_optimizer if not any(nd in n for nd in no_decay)],
         'weight_decay': 0.01},
        {'params': [p for n, p in param_optimizer if any(nd in n for nd in no_decay)], 'weight_decay': 0.0}
    ]

    if args.fp16:
        try:
            from apex.optimizers import FP16_Optimizer
            from apex.optimizers import FusedAdam
        except ImportError:
            raise ImportError(
                "Please install apex from https://www.github.com/nvidia/apex to use distributed and fp16 training.")

        optimizer = FusedAdam(optimizer_grouped_parameters,
                              lr=args.learning_rate,
                              bias_correction=False,
                              max_grad_norm=1.0)
        if args.loss_scale == 0:
            optimizer = FP16_Optimizer(optimizer, dynamic_loss_scale=True)
        else:
            optimizer = FP16_Optimizer(optimizer, static_loss_scale=args.loss_scale)
    else:
        optimizer = AdamW(optimizer_grouped_parameters, lr=args.learning_rate, eps=args.adam_epsilon)
    scheduler = get_linear_schedule_with_warmup(optimizer,
                                                num_warmup_steps=args.warmup_steps,
                                                num_training_steps=num_train_optimization_steps)
    loss_dict = defaultdict(list)
    global_step = 0
    #logging.info("***** Running training *****")
    #logging.info(f"  Num examples = {total_train_examples}")
    #logging.info("  Batch size = %d", args.train_batch_size)
    #logging.info("  Num steps = %d", num_train_optimization_steps)
    model.train()
    for epoch in range(args.epochs):
        epoch_dataset = PregeneratedDataset(epoch=epoch, training_path=args.pregenerated_data, tokenizer=tokenizer,
                                            num_data_epochs=num_data_epochs, reduce_memory=args.reduce_memory)
        if args.local_rank == -1:
            train_sampler = RandomSampler(epoch_dataset)
        else:
            train_sampler = DistributedSampler(epoch_dataset)
        train_dataloader = DataLoader(epoch_dataset, sampler=train_sampler, batch_size=args.train_batch_size, num_workers=NUM_CPU)
        tr_loss = 0
        nb_tr_examples, nb_tr_steps = 0, 0
        with tqdm(total=len(train_dataloader), desc=f"Epoch {epoch}") as pbar:
            for step, batch in enumerate(train_dataloader):
                batch = tuple(t.to(device) for t in batch)
                input_ids, input_mask, lm_label_ids = batch
                
                outputs = model(input_ids=input_ids, attention_mask=input_mask, masked_lm_labels=lm_label_ids)
                loss = outputs[0]
                #if n_gpu > 1:
                #    loss = loss.mean() # mean() to average on multi-gpu.
                if args.gradient_accumulation_steps > 1:
                    loss = loss / args.gradient_accumulation_steps
                if args.fp16:
                    optimizer.backward(loss)
                else:
                    loss.backward()
                tr_loss += loss.item()
                nb_tr_examples += input_ids.size(0)
                nb_tr_steps += 1
                pbar.update(1)
                mean_loss = tr_loss * args.gradient_accumulation_steps / nb_tr_steps
                pbar.set_postfix_str(f"Loss: {mean_loss:.5f}")
                if (step + 1) % args.gradient_accumulation_steps == 0:
                    optimizer.step()
                    scheduler.step()  # Update learning rate schedule
                    optimizer.zero_grad()
                    global_step += 1
                loss_dict["epoch"].append(epoch)
                loss_dict["batch_id"].append(step)
                loss_dict["mlm_loss"].append(loss.item())
        # Save a trained model
        if epoch < num_data_epochs:
            logging.info("** ** * Saving fine-tuned model ** ** * ")
            epoch_output_dir = args.output_dir / f"epoch_{epoch}"
            epoch_output_dir.mkdir(parents=True, exist_ok=True)
            model.save_pretrained(epoch_output_dir)
            tokenizer.save_pretrained(epoch_output_dir)

    # Save a trained model
    if n_gpu > 1 : #and torch.distributed.get_rank() == 0 or n_gpu <=1:
        logging.info("** ** * Saving fine-tuned model ** ** * ")
        model.save_pretrained(args.output_dir)
        tokenizer.save_pretrained(args.output_dir)
        df = pd.DataFrame.from_dict(loss_dict)
        df.to_csv(args.output_dir/"losses.csv")

이제 data가 준비되었으니 모델을 불러오고 학습을 진행하도록 만들었다.

논문에서 pre-trained된 모델인 bertforMLMPreTraining 을 사용했는데, 이는 github 내에 있는 fine tuning과정 안에 포함된 함수이다. 즉, 이 task를 위해 기존에 사전 훈련되어 있던 모델을 불러오는 과정이라고 생각하면 된다.

우리는 여러 task 중에 adjective sentiment task를 수행하기 때문에 specific fine tuning을 진행한다고 보면 된다.

from types import SimpleNamespace
args = SimpleNamespace()
#args.train_corpus 
#args.output_dir
args.bert_model = BERT_PRETRAINED_MODEL
args.do_lower_case = True
args.reduce_memory = False
args.epochs= int(EPOCHS)
args.local_rank = -1
args.no_cuda = False
args.gradient_accumulation_steps = int(1)
args.train_batch_size = int(BATCH_SIZE)
args.fp16 = False
args.loss_scale = float(0)
args.warmup_steps = int(0)
args.adam_epsilon = float(1e-8)
args.learning_rate = float(3e-5)
args.seed = int(RANDOM_SEED)
args.masking_method = str('double_num_adj')
args.pregenerated_data = Path(SENTIMENT_ADJECTIVES_PRETRAIN_DATA_DIR) / args.masking_method
args.output_dir = Path(SENTIMENT_ADJECTIVES_PRETRAIN_MLM_DIR) / args.masking_method / "model"
args.fp16 = FP16
print(args)

pretrain_on_treatment(args)

모델을 훈련시키고 나면 epoch0 / epoch1과 같은 폴더들이 생성되는데, 이는 epoch마다 모델이 저장된 것을 의미한다.


프로젝트 진행 - IMA_task


이 후, IMA_task도 진행했으며 이 또한 MLM task와 마찬가지로 사전 훈련된 모델들을 fine-tuning하는 과정이라고 보면 된다. 코드를 모두 첨부하기에는 길기 때문에, github를 참고하면 좋겠다.


프로젝트 진행 - training 진행


앞서 MLM task와 IMA task에서 훈련된 모델들을 사용하여, 본격적인 causaLM 모델을 훈련시켜야 한다.

from constants import SENTIMENT_EXPERIMENTS_DIR, MAX_SENTIMENT_SEQ_LENGTH, SENTIMENT_ADJECTIVES_PRETRAIN_IMA_DIR, \
    SENTIMENT_ADJECTIVES_DATASETS_DIR
from pytorch_lightning import Trainer
from BERT.bert_text_classifier import LightningBertPretrainedClassifier, LightningHyperparameters
from BERT.bert_pos_tagger import LightningBertPOSTagger
from Sentiment_Adjectives.pipeline.predict import predict_models, print_final_metrics

from argparse import ArgumentParser
from typing import Dict
import torch

from datasets.utils import NUM_POS_TAGS_LABELS
from utils import init_logger

DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")

### Constants
BATCH_SIZE = 128
ACCUMULATE = 4
DROPOUT = 0.1
EPOCHS = 50
FP16 = False

필요한 package와 hyper parameter들을 설정한다.

def bert_train_eval(hparams, task, output_dir):
    trainer = Trainer(gpus=1 if DEVICE.type =='cuda' else 0,
                      default_save_path=output_dir,
                      show_progress_bar=True,
                      accumulate_grad_batches=hparams['accumulate'],
                      max_nb_epochs=hparams['epochs'],
                      early_stop_callback=None)
    hparams['output_path'] = trainer.logger.experiment.log_dir.rstrip('tf')
    logger = init_logger("training",hparams['output_path'])
    logger.info(f"Training {task} for {hparams['epochs']} epochs")
    if task == "Sentiment":
        hparams['bert_params']['batch_size'] = hparams['batch_size']
        model = LightningBertPretrainedClassifier(LightningHyperparameters(hparams))
    
    else:
        model = LightningBertPOSTagger(LightningHyperparameters(hparams))
    trainer.fit(model)
    trainer.test()
    print_final_metrics(hparams['bert_params']['name'], trainer.tqdm_metrics, logger)
    return model

def train_models_unit(hparams: Dict, task, group, pretrained_control):
    label_size = 2
    if task == "POS_Tagging":
        label_size = NUM_POS_TAGS_LABELS
        label_column = f"{task.lower()}_{group.lower()}_labels"
    elif task == "IMA":
        label_column = f"{task.lower()}_{group.lower()}_labels"
    else:
        label_column = f"{task.lower()}_label"
    
    hparams['label_column'] = label_column
    hparams['num_labels'] = label_size
    hparams['bert_params']['label_size'] = label_size
    
    if hparams['bert_params']['bert_state_dict']:
        if pretrained_control:
            hparams['bert_params']['name'] = f"{task}_{group}_ima_control_treated"
        else:
            hparams['bert_params']['name'] = f"{task}_{group}_ima_treated"
        
    else:
        hparams['bert_params']['name'] = f"{task}_{group}"
    
    OUTPUT_DIR = f"{SENTIMENT_EXPERIMENTS_DIR}/{hparams['treatment']}/{hparams['bert_params']['name']}"
    model = bert_train_eval(hparams, task, OUTPUT_DIR)
    return model
    
def train_models(hparams: Dict, group: str, pretrained_masking_method, pretrained_epoch: int, pretrained_control: bool):
    print(f"Training {hparams['treatment']} models")
    sentiment_model = train_models_unit(hparams, "Sentiment",group,pretrained_control)
    ima_model = train_models_unit(hparams,"IMA",group, pretrained_control)
    pos_tagging_model = train_models_unit(hparams, "POS_Tagging", group, pretrained_control)
    
    if hparams['bert_params']['bert_state_dict']:
        if pretrained_control:
            group = f"{group}_ima_control_treated"
        else:
            group = f"{group}_ima_treated"
    predict_models(hparams['treatment'], group,
                   pretrained_masking_method, pretrained_epoch, pretrained_control,
                   sentiment_model, ima_model, pos_tagging_model,
                   hparams["bert_params"]["bert_state_dict"])

def train_all_models(args):
    treatment = 'adjectives'
    if args.group == 'F':
        text_column = 'review'
    else:
        text_column = "no_adj_review"
    if args.pretrained_control:
        pretrained_treated_model_dir = f"{SENTIMENT_ADJECTIVES_PRETRAIN_IMA_DIR}/{args.masking_method}/model_control"
    else:
        pretrained_treated_model_dir = f"{SENTIMENT_ADJECTIVES_PRETRAIN_IMA_DIR}/{args.masking_method}/model"

    if args.pretrained_epoch is not None:
        pretrained_treated_model_dir = f"{pretrained_treated_model_dir}/epoch_{args.pretrained_epoch}"
    
    hparams = {
        "data_path": SENTIMENT_ADJECTIVES_DATASETS_DIR,
        "treatment": treatment,
        "masking_method": args.masking_method,
        "pretrain_conrol": args.pretrained_control,
        "text_column": text_column,
        "label_column": "sentiment_label",
        "batch_size": args.batch_size,
        "epochs": args.epochs,
        "accumulate": ACCUMULATE,
        "max_seq_len": MAX_SENTIMENT_SEQ_LENGTH,
        "num_labels": 2,
        "name": f"Sentiment_{args.group}",
        "bert_params": {
            "dropout": DROPOUT,
            "bert_state_dict": None,
            "label_size": 2,
            "name": f"Sentiment_{args.group}"
        }
    }
    
    train_models(hparams, args.group, args.masking_method, args.pretrained_epoch, args.pretrained_control)
    
    hparams['bert_params']['bert_state_dict'] = f"{pretrained_treated_model_dir}/pytorch_model.bin"
    hparams['treatment']= treatment
    train_models(hparams, args.group, args.masking_method, args.pretrained_epoch, args.pretrained_control)

training에 필요한 함수들을 정의하는데, 앞선 code study에서도 보면 알겠지만 모두 연결되어 있는 함수임을 알 수 있다. 결론적으로, train_all_models를 사용한다고 보면 된다.

from types import SimpleNamespace
args = SimpleNamespace()
args.treatment = 'adjectives'
args.group = 'F'
args.masking_method = 'double_num_adj'
args.pretrained_epoch = 0
args.pretrained_control = True
args.batch_size = BATCH_SIZE
args.epochs = EPOCHS


train_all_models(args)

이렇게 우리가 필요한 모델들을 훈련했으며, constant 파일 안에 존재하는 모델 명을 바꿔가며 BERT-large로도 훈련을 진행했다.


프로젝트 결과


MLM task로 fine tuning된 모델의 결과
IMA task에서 fine tuning된 모델의 결과
Aggressive data로 훈련된 모델에 balanced data로 test한 결과 (accuracy)
Counterfactual task (가장 큰 task)에서 모델을 바꿔가며 얻은 결과

ATE와 TReATE가 유사할 수록 좋은데, 본 논문의 결과에 미치진 못했다.특히 aggressive에서 차이가 많이 났는데, 기존 논문에서도 aggressive의 차이가 가장 커서 어느정도 합리적인 결과라고 생각했다.

나머지 부분들은 paper보다 더 성능이 좋게 나왔는데, 아마 dataset을 구축하는 과정에서나 dataset을 사용하여 fine tuning하는 과정에서 변화가 있었던 게 아닐까 생각했다. 성능이 안 좋은 이유를 찾는 것도 어려운데, 왜 성능이 더 좋게 나왔는 지 찾는건 더 어려웠다. 

이 부분에 대해서는 추가적인 공부가 필요했지만, 시간이 없는 관계로 패스했다.


프로젝트를 진행하며 느낀 점


인공지능을 전공하면서 본격적으로 제대로 된 프로젝트를 진행해봤다는 생각을 했다.

학부생 신분으로 사용할 수 있는 컴퓨팅 자원은 colab pro까지가 최대였는데, 실제 서버의 GPU들을 사용해서 colab으로 돌릴 수 없는 나름 큰 causal LM을 돌려볼 수 있어서 좋았고, 이 과정에서 정말 많은 점들을 배웠다.

  • 서버 연결
  • 환경 구축
  • GPU 사용
  • large model에 대한 parameter 설정
  • dataset 구축
  • train 과정 숙지
  • 기타

조금 두루뭉실한 것 같긴 하지만, 정말 저것 외에도 많은 것들을 배울 수 있어서 좋았다. 특히 최근 관심을 가지게 된 NLP 분야에서 causal inference라는 새로운 분야를 알 수 있었고 관련 이론들을 익힐 수 있어서 좋았다.

그리고 무엇보다 논문 -> 코드 공부 -> reproducing -> task 라는 전형적인 과정을 통해 정말 내가 이론에서 배운 것들을 써볼 수 있구나.. 싶은 느낌이 들어서 행복한 경험이었다.

 

물론, 10월이 지난 지금은 large BERT 학습 및 DALL-E 모델 구현 등의 큰 모델들을 다루는 프로젝트를 2개 진행 중이다. 이와 관련된 포스팅도 추후 프로젝트가 완료되면 올릴 계획이다.

 

길고 두서없는 내용들이었을 것 같은데, 여기까지 읽으신 분들이 계시다면 편하게 피드백해주시면 감사하겠습니다.

728x90
반응형