<jupyter_start><jupyter_text>"[DACON] 자연어 처리 문장 쌍 분류하기2 With Pytorch"- author: <NAME> - categories: [jupyter, Deep Learning, Pytorch, DACON, natural language, BERT, tokenizer, Classifier]  패키지 설치하기<jupyter_code>from google.colab import drive
drive.mount('/content/drive')<jupyter_output>Mounted at /content/drive<jupyter_text>구글 드라이브와 연동합니다.<jupyter_code>!pip install mxnet-cu101
!pip install gluonnlp pandas tqdm
!pip install sentencepiece==0.1.85
!pip install transformers==2.1.1
!pip install git+https://git@github.com/SKTBrain/KoBERT.git@master

import pandas as pd
import numpy as np

from tqdm import tqdm, tqdm_notebook

import torch
from torch import nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader

import gluonnlp as nlp

#kobert
from kobert.utils import get_tokenizer
from kobert.pytorch_kobert import get_pytorch_kobert_model

#transformers
from transformers import AdamW
# 스케줄러 조정 함수
from transformers.optimization import get_cosine_schedule_with_warmup


import warnings
warnings.filterwarnings("ignore")<jupyter_output>Collecting mxnet-cu101
  Downloading mxnet_cu101-1.9.0-py3-none-manylinux2014_x86_64.whl (358.1 MB)
[K     |████████████████████████████████| 358.1 MB 4.7 kB/s 
[?25hRequirement already satisfied: numpy<2.0.0,>1.16.0 in /usr/local/lib/python3.7/dist-packages (from mxnet-cu101) (1.21.5)
Requirement already satisfied: requests<3,>=2.20.0 in /usr/local/lib/python3.7/dist-packages (from mxnet-cu101) (2.23.0)
Collecting graphviz<0.9.0,>=0.8.1
  Downloading graphviz-0.8.4-py2.py3-none-any.whl (16 kB)
Requirement already satisfied: chardet<4,>=3.0.2 in /usr/local/lib/python3.7/dist-packages (from requests<3,>=2.20.0->mxnet-cu101) (3.0.4)
Requirement already satisfied: idna<3,>=2.5 in /usr/local/lib/python3.7/dist-packages (from requests<3,>=2.20.0->mxnet-cu101) (2.10)
Requirement already satisfied: urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1 in /usr/local/lib/python3.7/dist-packages (from requests<3,>=2.20.0->mxnet-cu101) (1.24.3)
Requirement already satisfied: certifi>=2017.4.17 in /usr/local/[...]<jupyter_text>필요한 패키지를 설치하고 임포트 합니다.  데이터 불러오기<jupyter_code>path = '/content/drive/MyDrive/sentence/'

train = pd.read_csv(path + 'train_data.csv')
test = pd.read_csv(path + 'test_data.csv')
sample_submission = pd.read_csv(path + 'sample_submission.csv')

train.head()<jupyter_output><empty_output><jupyter_text>데이터를 불러옵니다. 데이터에 대한 설명은 이전에 했으니 생략합니다.<jupyter_code>label_dict = {'entailment' : 0, 'contradiction' : 1, 'neutral' : 2}

train_content = []
test_content = []

for idx in tqdm(train.index):
    train_content.append(list([[train.loc[idx, 'premise'], train.loc[idx, 'hypothesis']],
                               label_dict[train.loc[idx, 'label']]]))

for idx in tqdm(test.index):
    test_content.append(list([[test.loc[idx, 'premise'], test.loc[idx, 'hypothesis']]]))


dataset_train = train_content[:20000]
dataset_valid = train_content[20000:]
dataset_test = test_content

dataset_train[:5]<jupyter_output>100%|██████████| 24998/24998 [00:01<00:00, 23116.36it/s]
100%|██████████| 1666/1666 [00:00<00:00, 39118.13it/s]<jupyter_text>입력된 데이터를 리스트 형태로 다시 구성합니다. 이때 [[전제, 가설], 라벨] 구성으로 데이터 한 세트를 만듭니다.2만개를 트레인 세트, 나머지 약 5천개를 valid 세트로 구성합니다.  데이터 로더 구축하기<jupyter_code>max_len = 70
batch_size = 64
warmup_ratio = 0.1
num_epochs = 5
max_grad_norm = 1
log_interval = 200
learning_rate = 5e-5

if torch.cuda.is_available():
    device = torch.device('cuda:0')

else:
    device = torch.device('cpu')

# kobert 패키지 내 BERT 모델과 어휘 집합을 입력받습니다. 
bertmodel, vocab = get_pytorch_kobert_model(cachedir = '.cache')

# kobert 패키지 내 토크나이저를 입력받습니다.
tokenizer = get_tokenizer()

# 입력받은 토크나이저를 입력받은 어휘 집합을 이용해 학습합니다.
tok = nlp.data.BERTSPTokenizer(tokenizer, vocab, lower = False)<jupyter_output>/content/.cache/kobert_v1.zip[██████████████████████████████████████████████████]
/content/.cache/kobert_news_wiki_ko_cased-1087f8699e.spiece[██████████████████████████████████████████████████]
using cached model. /content/.cache/kobert_news_wiki_ko_cased-1087f8699e.spiece<jupyter_text>기본적인 하이퍼 파라미터 설정을 미리 합니다. 문장 최대 길이, 에포크, 러닝레이트 등등을 미리 지정합니다.또 kobert 패키지 내 BERT 모델, 토크나이저, 어휘 집합을 입력받습니다.<jupyter_code>class BERTDataset(Dataset):
    def __init__(self, dataset, sen_idx, label_idx, bert_tokenizer, max_len,
                 pad, pair, mode = 'train'):
        
        self.mode = mode
        transform = nlp.data.BERTSentenceTransform(bert_tokenizer, max_seq_length = max_len,
                                                   pad = pad, pair = pair)
        # 문장 쌍(pair = True)을 학습하는 트렌스포머를 만듭니다.
        
        if self.mode == 'train':
            self.sentence = [transform(i[sen_idx]) for i in dataset]
            self.labels = [np.int32(i[label_idx]) for i in dataset]

        else:
            self.sentence = [transform(i[sen_idx]) for i in dataset]


    def __getitem__(self, i):
        if self.mode == 'train':
            return (self.sentence[i] + (self.labels[i],))

        else:
            return self.sentence[i]

    def __len__(self):
        return (len(self.sentence))<jupyter_output><empty_output><jupyter_text>파이토치 내 Dataset 클래스를 상속받아서 데이터 셋을 클래스로 만듭니다.Dataset 클래스를 상속했기 때문에 향후 DataLoader 내에 실을 수 있겠죠.리스트 형태인 데이터 내 문장을 트랜스포머를 이용해서 변환 한 뒤 sentence 내 리스트 형태로 저장합니다.라벨 값 또한 labels 내 리스트 형태로 저장합니다.<jupyter_code># (리스트 형태 데이터 셋, 문장위치, 라벨위치, 토크나이저, 문장 최대 길이, pad, pair, 모드)
data_train = BERTDataset(dataset_train, 0, 1, tok, max_len, True, True, mode = 'train')
data_valid = BERTDataset(dataset_valid, 0, 1, tok, max_len, True, True, mode = 'train')
data_test = BERTDataset(dataset_test, 0, 1, tok, max_len, True, True, mode = 'test')

train_dataloader = torch.utils.data.DataLoader(data_train, batch_size = batch_size, num_workers = 0)
valid_dataloader = torch.utils.data.DataLoader(data_valid, batch_size = batch_size, num_workers = 0)
test_dataloader = torch.utils.data.DataLoader(data_test, batch_size = batch_size, num_workers = 0)<jupyter_output><empty_output><jupyter_text>앞서 정의한 BERTDataset 클래스를 이용해 문장을 트랜스포머로 변형하고, 로더에 실겠습니다.<jupyter_code>data_train[0]<jupyter_output><empty_output><jupyter_text>이 데이터 셋은 문장을 트랜스포머를 통해 숫자로 바꾼 값, 전체 문장이 끝나는 위치, 두번째 문장을 1로 표기한 리스트, 라벨값으로 구성됩니다.  모델 구축하기<jupyter_code>class BERTClassifier(nn.Module):
    def __init__(self, bert, hidden_size = 768, num_classes = 3, dr_rate = None, params = None):
        super(BERTClassifier, self).__init__()
        self.bert = bert
        self.dr_rate = dr_rate

        self.classifier = nn.Linear(hidden_size, num_classes)
        if dr_rate:
            self.dropout = nn.Dropout(p = dr_rate)

    def gen_attention_mask(self, token_ids, valid_length):
        attention_mask = torch.zeros_like(token_ids)
        for i, v in enumerate(valid_length):
            attention_mask[i][:v] = 1
        return attention_mask.float()


    def forward(self, token_ids, valid_length, segment_ids):
        attention_mask = self.gen_attention_mask(token_ids, valid_length)

        _, pooler = self.bert(input_ids = token_ids, token_type_ids = segment_ids.long(),
                              attention_mask = attention_mask.float().to(token_ids.device))
        if self.dr_rate:
            out = self.dropout(pooler)

        return self.classifier(out)<jupyter_output><empty_output><jupyter_text>파이토치 내 nn.Module 클래스를 상속받아 모델을 클래스로 구현했습니다. init 함수로 정의를 하고 forward 함수에서 실행을 합니다.특이한 점은 bert 모델에 입력값으로 input_ids(숫자로 변환된 문장), token_type_ids(두번째 문장을 1로 표기한 리스트), attention_mask(문장부분을 1로, 문장 아닌부분을 0으로 표기한 리스트) 세가지를 받습니다.현재 데이터 로더는 input_ids, token_type_ids는 bert 입력값과 같은형태이나, attention_mask가 아닌 문장이 끝나는 부분을 숫자로 알려줍니다.문장이 끝나는 부분 값을 attention_mask 형식으로 바꾸기 위해 gen_attention_mask 함수를 사용했습니다.<jupyter_code>model = BERTClassifier(bertmodel, dr_rate = 0.5).to(device)

# 가중치 감퇴를 위한 부분 (오버피팅 방지를 위해)
no_decay = ['bias', 'LayerNorm.weight'] # 이 부분은 가중치 감퇴 x
optimizer_group_parameters = [
    {'params': [p for n, p in model.named_parameters() if not any(nd in n for nd in no_decay)],
     'weight_decay' : 0.01},
    {'params': [p for n, p in model.named_parameters() if any(nd in n for nd in no_decay)],
     'weight_decay' : 0.0}
]

optimizer = AdamW(optimizer_group_parameters, lr = learning_rate)
loss_fn = nn.CrossEntropyLoss()

# warnup 값을 설정해줍니다.
t_total = len(train_dataloader) * num_epochs
warmup_step = int(t_total * warmup_ratio)

# warnup 값보다 스텝이 작을경우 러닝레이트를 선형증가, 클경우 러닝레이트 임의에 방법으로 업데이트 합니다.
scheduler = get_cosine_schedule_with_warmup(optimizer, num_warmup_steps = warmup_step,
                                            num_training_steps = t_total)<jupyter_output><empty_output><jupyter_text>모델을 정의하고, 자연어 분류문제에 자주 사용되는 AdamW 옵티마이저와 크로스엔트로피 손실함수를 정의합니다.오버피팅 방지를 위해 가중치를 규제하는 부분도 정의했으며, 러닝레이트 또한 스케줄러를 통해 유동적으로 조정해줍니다.  모델 학습하기<jupyter_code>def calc_accuracy(x,y):
    max_vals, max_indices = torch.max(x, 1)
    train_acc = (max_indices == y).sum().data.cpu().numpy() / max_indices.size()[0]
    return train_acc<jupyter_output><empty_output><jupyter_text>모델 성능 확인을 위해 정확도를 계산해주는 함수를 만들었습니다.<jupyter_code>import enum
for e in range(num_epochs):
    train_acc = 0.0
    valid_acc = 0.0
    model.train()

    for batch_id, (token_ids, valid_length, segment_ids, label) in tqdm(enumerate(train_dataloader), total = len(train_dataloader)):
        optimizer.zero_grad() # 옵티마이저 파라미터 배치단위로 초기화
        token_ids = token_ids.long().to(device) 
        segment_ids = segment_ids.long().to(device)
        valid_length = valid_length
        label = label.long().to(device)
        out = model(token_ids, valid_length, segment_ids)
        loss = loss_fn(out, label)
        loss.backward()

        # Gradient Vanishing 또는 Exploding 방지
        torch.nn.utils.clip_grad_norm_(model.parameters(), max_grad_norm)

        optimizer.step()
        scheduler.step() # 러닝 레이트 업데이트
        train_acc += calc_accuracy(out, label)

    print("epoch {} train acc {}".format(e+1, train_acc / (batch_id+1)))
    
    model.eval()

    for batch_id, (token_ids, valid_length, segment_ids, label) in tqdm(enumerate(valid_dataloader), total = len(valid_dataloader)):
        token_ids = token_ids.long().to(device)
        segment_ids = segment_ids.long().to(device)
        valid_length = valid_length
        label = label.long().to(device)
        out = model(token_ids, valid_length, segment_ids)
        valid_acc += calc_accuracy(out, label)

    print("epoch {} valid acc {}".format(e+1, valid_acc / (batch_id+1)))<jupyter_output>100%|██████████| 313/313 [07:05<00:00,  1.36s/it]<jupyter_text>데이터 로더에서 배치단위로 값을 꺼내서 모델에 적용하고, 가중치를 업데이트 시켜줬습니다.한 에포크가 끝날때마다 정확도를 지속적으로 확인하는 모습입니다.  모델 이용하기<jupyter_code>result = []
model.eval()
with torch.no_grad():
    for batch_id, (token_ids, valid_length, segment_ids) in tqdm(enumerate(test_dataloader), total = len(test_dataloader)):
        token_ids = token_ids.long().to(device)
        segment_ids = segment_ids.long().to(device)
        valid_length = valid_length
        result.append(model(token_ids, valid_length, segment_ids))<jupyter_output>100%|██████████| 27/27 [00:12<00:00,  2.11it/s]<jupyter_text>모델을 평가모드로 변환하고 테스트 데이터를 모델에 넣어 3개 라벨의 예측 확률을 출력한 것을 result 안에 넣습니다.<jupyter_code>result_ = []

for i in result:
    for j in i:
        result_.append(int(torch.argmax(j)))

out = [list(label_dict.keys())[_] for _ in result_]

sample_submission['label'] = out

sample_submission.to_csv('sentence_4.csv',index=False)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.13194750286267948, 'fraction_numerical': 0.029331454241169733, 'mean_word_length': 4.0664881749219095, 'pattern_counts': {'":': 0, '<': 65, '<?xml version=': 0, '>': 65, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 1, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '14', 'dir': 'jupyter-structured-clean-dedup', 'id': '622973', 'license': 'Apache-2.0', 'n_tokens_mistral': 5455, 'n_tokens_neox': 5929, 'n_words': 953, 'path': '_notebooks/2022-02-21-torch3.ipynb', 'repo_name': 'KSY1526/myblog', 'size': '11361'}