얼마전에 koBERT로 colab에서 학습하였는데 어떻게 로컬에서 옮겨야 할지 감이 안잡혔다.
mxnet, glounnlp를 직접 다운로드하여 라이브러리에 넣어주었는데도 안되고
vmware를 깔아서 리눅스환경에서 해보았는데도 잘 안되었다.
그래서 koBERT는 아쉽지만 잠시 모델만 남겨두고 다른 방법으로 로컬로 학습을 시키려고 한다.
koBERT 모델 학습하기에서 txt파일을 만들었는데 그것을 사용하겠다.
인공지능 koBERT 모델 학습
추천시스템에 쓰일 '태그'를 달기 위해 모델을 하나 제작하고 있다. 다른 모델들도 많지만 koELECTRA와 기타 모델은 데이터 전처리를 모델에 맞게 해주지 않아서 그런가 정확도가 높지 않았다. 그
beomcoder.tistory.com
0. 데이터 처리하기
koBERT 모델 학습하기에서 했던 데이터를 처리하는 코드이다.
AIHUB에서 '주제별 텍스트 일상 대화 데이터'를 다운로드 받았다.
그 중에서 나는 json형식으로 된 파일만 사용한다. 본인이 쓸 부분을 가지고 처리하면 된다. 나는 폴더를 vscode에 넣었다.
# aihub의 데이터 중 카테고리가 '상거래전반'에서 띄어쓰기를 한것과 안한것이 공존하여 처리해줌
category = { '식음료': 0, '주거와 생활': 1, '교통': 2, '회사/아르바이트': 3, '군대': 4, '교육': 5, '가족': 6, '연애/결혼': 7, '반려동물': 8, '스포츠/레저': 9,
'게임': 10, '여행': 11, '계절/날씨': 12, '사회이슈': 13, '타 국가 이슈': 14, '미용': 15, '건강': 16, '상거래전반': 17, '상거래 전반': 17, '방송/연예': 18,
'영화/만화': 19 }
카테고리는 aihub에서 카테고리를 그대로 가지고 왔는데 중간에 띄어쓰기로 적혀있는 부분이 있어서
그 부분은 따로 같은 번호로 처리해주었다.
# AIHUB에서 가지고온 json파일을 text file로 변환하기 위해 txt file 생성
txt_file_names = ['train_data', 'valid_data']
for name in txt_file_names:
f = open(name+'.txt', 'w')
f.write('id\ttext\tlabel\n')
f.close()
train data와 valid data를 다르게 처리하려고 2개를 만들었다. 나중에 pandas dataframe으로
컬럼값으로 id, text, label으로 설정해주려고 텍스트 제일 윗줄에 적어주었다.
# txt로 만든 이유는 다른 곳에서도 데이터를 사용할 수도 있기 때문에 txt파일로 만들었다.
import os, json
dirs = ['dataset\\1.Training\\라벨링데이터', 'dataset\\2.Validation\\라벨링데이터']
for txt_file_name, dir_name in zip(txt_file_names, dirs):
f = open(txt_file_name+'.txt', 'a', encoding='UTF-8')
id_count = 0
for root, dir, filenames in os.walk(dir_name):
for filename in filenames:
path = os.path.join(root, filename)
with open(path, 'r', encoding='UTF-8') as file:
try:
json_data = json.load(file)
except:
print(path)
continue
for info in json_data['info']:
label = category[info['annotations']['subject']]
for line in info['annotations']['lines']:
text = line['norm_text']
f.write(f'{id_count}\t{text}\t{label}\n')
print(f'\r{id_count} ', end='')
id_count += 1
file.close()
f.close()
처음엔 예외처리를 안해주었는데, 중간에 파일 몇개가 읽어지지 않아서 try ~ except 구문으로 처리해주었다.
!pip install transformers
import tensorflow as tf
import torch
from transformers import BertTokenizer
from transformers import BertForSequenceClassification, AdamW, BertConfig
from transformers import get_linear_schedule_with_warmup
from torch.utils.data import TensorDataset, DataLoader, RandomSampler, SequentialSampler
# from keras.preprocessing.sequence import pad_sequences # 로컬환경에서 에러가 난다.
from keras_preprocessing.sequence import pad_sequences
from sklearn.model_selection import train_test_split
import pandas as pd
import numpy as np
import random
import time
import datetime
import csv
import pandas as pd
train_data = pd.read_csv('train_data.txt', encoding='utf-8', sep='\t').dropna(axis=0)
pandas csv파일을 읽어오는 방식인데 txt파일도 상관없다. 구분자는 tab키로 해두었기 때문에 sep='\t'로 해주었다.
띄어쓰기로 구분하면 채팅 띄어쓰기와 구분할 수 없기 때문이다. 그리고 nan 행이 없을 줄 알았는데
나중에 학습을 해주니 nan행이 발견되어 dropna로 nan행을 없애주었다.
train_data['label'] = train_data['label'].astype(np.int32)
label을 0~19로 적어주었는데 train.head()로 확인해보니 float형으로 나와있어서 int형으로 바꾸어주었다.
float형으로도 되는지는 잘 모르지만 보통 내가 보기에 label은 int형으로 많이 적혀있어서 그렇게 처리해주었다.
sentences = ["[CLS] " + str(s) + " [SEP]" for s in train_data['text']]
labels = train_data['label'].values
BERT모델의 경우 문장의 앞마다 [CLS]를 붙여 인식하고, [SEP]표시로 문장의 종료를 인식한다.
그래서 문장의 앞가 뒤에 표시를 해주었다.
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained("bert-base-multilingual-cased", do_lower_case=False)
tokenized_texts = [tokenizer.tokenize(s) for s in sentences]
print(sentences[0])
print(tokenized_texts[0])
# [CLS] 애덜앙 나 너무 배불러서 배 아파 [SEP]
# ['[CLS]', '애', '##덜', '##앙', '나', '너', '##무', '배', '##불', '##러', '##서', '배', '아', '##파', '[SEP]']
tokenizer를 가지고와서 우리 문장을 토크나이징을 한다. 토크나이징은 단어집합에 있는 단어는
한 묶음으로 묶어주고, 단어 집합에 없는 단어들은 쪼개준다. 집합에 없는 단어는 쪼갤때 ##를 붙여서
다른 단어에서 쪼개져 나왔음을 알려주는 것이다.
# 문장의 최대 시퀀스를 설정하여 정수 인코딩과 제로 패딩을 수행
MAX_LEN = 128 #최대 시퀀스 길이 설정
input_ids = [tokenizer.convert_tokens_to_ids(x) for x in tokenized_texts]
input_ids = pad_sequences(input_ids, maxlen=MAX_LEN, dtype="long", truncating="post", padding="post")
attention_masks = []
for seq in input_ids:
seq_mask = [float(i>0) for i in seq]
attention_masks.append(seq_mask)
어텐션 마스크란 0 값을 가지는 패딩 토큰에 대해서 어텐션 연산을 불필요하게 수행하지 않도록
단어와 패딩 토큰을 구분할 수 있게 알려주는 것을 말한다.
따라서 [40311, 9435, 102, 0, 0]와 같은 패딩된 데이터가 있을 때,
패딩된 값은 '0', 패딩되지 않은 단어는 '1'의 값을 갖도록 시리얼 데이터를 만들어 주어야 한다.
train_inputs, validation_inputs, train_labels, validation_labels = train_test_split(input_ids, labels, random_state=2000, test_size=0.1)
train_masks, validation_masks, _, _ = train_test_split(attention_masks, input_ids, random_state=2000, test_size=0.1)
train_inputs = torch.tensor(train_inputs)
train_labels = torch.tensor(train_labels)
train_masks = torch.tensor(train_masks)
validation_inputs = torch.tensor(validation_inputs)
validation_labels = torch.tensor(validation_labels)
validation_masks = torch.tensor(validation_masks)
train과 valid를 나누어준다. test셋은 처음에 만들었는데, 데이터가 너무 많아
이 글을 적고 있는 중에도 학습이 진행중이다. 그래서 test셋은 하지 않으려고 만들어주지 않았다.
데이터가 적거나 시간이 많은 분들은 test 셋도 이렇게 만들어 주면 된다.
batch_size = 1
train_data = TensorDataset(train_inputs, train_masks, train_labels)
train_sampler = RandomSampler(train_data)
train_dataloader = DataLoader(train_data, sampler=train_sampler, batch_size=batch_size)
validation_data = TensorDataset(validation_inputs, validation_masks, validation_labels)
validation_sampler = SequentialSampler(validation_data)
validation_dataloader = DataLoader(validation_data, sampler=validation_sampler, batch_size=batch_size)
마찬가지로 데이터가 너무 많아서 batch_size를 64로 하니까
용량문제로 진행이 되지 않아서 batch_size를 1로 맞추었다.
1. BERT모델 불러오기
n_devices = torch.cuda.device_count()
print(n_devices)
for i in range(n_devices):
print(torch.cuda.get_device_name(i))
if torch.cuda.is_available():
device = torch.device("cuda")
print('There are %d GPU(s) available.' % torch.cuda.device_count())
print('We will use the GPU:', torch.cuda.get_device_name(0))
else:
device = torch.device("cpu")
print('No GPU available, using the CPU instead.')
컴퓨터에서 GPU를 사용할 수 있는지 확인한다.
model = BertForSequenceClassification.from_pretrained("bert-base-multilingual-cased", num_labels=20)
model.cuda()
"""
...
)
)
(dropout): Dropout(p=0.1, inplace=False)
(classifier): Linear(in_features=768, out_features=20, bias=True)
"""
num_labels에서 본인의 카테고리 갯수를 적어준다. 제일 밑단 out_features에서 나올 갯수이다.
나는 카테고리를 20개로 분류해서 20개로 적어주었다.
이진 분류면 num_labels에서 2로 적어주면 된다.
# 옵티마이저
optimizer = AdamW(model.parameters(), lr = 2e-5, eps = 1e-8)
# 에폭수
epochs = 5
# 총 훈련 스텝 : 배치반복 횟수 * 에폭
total_steps = len(train_dataloader) * epochs
# 스케줄러 생성
scheduler = get_linear_schedule_with_warmup(optimizer, num_warmup_steps = 0, num_training_steps = total_steps)
# 정확도 계산 함수
def flat_accuracy(preds, labels):
pred_flat = np.argmax(preds, axis=1).flatten()
labels_flat = labels.flatten()
return np.sum(pred_flat == labels_flat) / len(labels_flat)
# 시간 표시 함수
def format_time(elapsed):
# 반올림
elapsed_rounded = int(round((elapsed)))
# hh:mm:ss으로 형태 변경
return str(datetime.timedelta(seconds=elapsed_rounded))
import gc
# Your code with pytorch using GPU
gc.collect()
자꾸 cuda out of mememory가 나와서 collect를 통해 처리해주었다.
2. 모델 학습하기
#랜덤시드 고정
seed_val = 42
random.seed(seed_val)
np.random.seed(seed_val)
torch.manual_seed(seed_val)
torch.cuda.manual_seed_all(seed_val)
#그래디언트 초기화
model.zero_grad()
# 학습
for epoch_i in range(0, epochs):
# ========================================
# Training
# ========================================
print("")
print('======== Epoch {:} / {:} ========'.format(epoch_i + 1, epochs))
print('Training...')
# 시작 시간 설정
t0 = time.time()
# 로스 초기화
total_loss = 0
# 훈련모드로 변경
model.train()
# 데이터로더에서 배치만큼 반복하여 가져옴
for step, batch in enumerate(train_dataloader):
# 경과 정보 표시
if step % 500 == 0 and not step == 0:
elapsed = format_time(time.time() - t0)
print(' Batch {:>5,} of {:>5,}. Elapsed: {:}.'.format(step, len(train_dataloader), elapsed))
# 배치를 GPU에 넣음
batch = tuple(t.to(device) for t in batch)
# 배치에서 데이터 추출
b_input_ids, b_input_mask, b_labels = batch
b_input_ids = b_input_ids.long().to(device)
b_input_mask = b_input_mask.long().to(device)
b_labels = b_labels.type(torch.LongTensor).to(device)
# Forward 수행
outputs = model(input_ids=b_input_ids, token_type_ids=None, attention_mask=b_input_mask, labels=b_labels)
# 로스 구함
loss = outputs[0]
# 총 로스 계산
total_loss += loss.item()
# Backward 수행으로 그래디언트 계산
loss.backward()
# 그래디언트 클리핑
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
# 그래디언트를 통해 가중치 파라미터 업데이트
optimizer.step()
# 스케줄러로 학습률 감소
scheduler.step()
# 그래디언트 초기화
model.zero_grad()
# 평균 로스 계산
avg_train_loss = total_loss / len(train_dataloader)
print("")
print(" Average training loss: {0:.2f}".format(avg_train_loss))
print(" Training epcoh took: {:}".format(format_time(time.time() - t0)))
# ========================================
# Validation
# ========================================
print("")
print("Running Validation...")
#시작 시간 설정
t0 = time.time()
# 평가모드로 변경
model.eval()
# 변수 초기화
eval_loss, eval_accuracy = 0, 0
nb_eval_steps, nb_eval_examples = 0, 0
# 데이터로더에서 배치만큼 반복하여 가져옴
for batch in validation_dataloader:
# 배치를 GPU에 넣음
batch = tuple(t.to(device) for t in batch)
# 배치에서 데이터 추출
b_input_ids, b_input_mask, b_labels = batch
# 그래디언트 계산 안함
with torch.no_grad():
# Forward 수행
outputs = model(b_input_ids,
token_type_ids=None,
attention_mask=b_input_mask)
# 로스 구함
logits = outputs[0]
# CPU로 데이터 이동
logits = logits.detach().cpu().numpy()
label_ids = b_labels.to('cpu').numpy()
# 출력 로짓과 라벨을 비교하여 정확도 계산
tmp_eval_accuracy = flat_accuracy(logits, label_ids)
eval_accuracy += tmp_eval_accuracy
nb_eval_steps += 1
print(" Accuracy: {0:.2f}".format(eval_accuracy/nb_eval_steps))
print(" Validation took: {:}".format(format_time(time.time() - t0)))
print("")
print("Training complete!")
# # 배치에서 데이터 추출
# b_input_ids, b_input_mask, b_labels = batch
#
# b_input_ids = b_input_ids.long().to(device)
# b_input_mask = b_input_mask.long().to(device)
# b_labels = b_labels.type(torch.LongTensor).to(device)
#
# # Forward 수행
# outputs = model(input_ids=b_input_ids, token_type_ids=None, attention_mask=b_input_mask, labels=b_labels)
# nll_loss forward_reduce_cuda_kernel_2d index not implemented for 'Int'
참고했던 코드에서는 b_input_ids, b_input_mask, b_labels를 그냥 넣었는데 나는 에러가 발생했다.
그래서 .long().to(device)로 처리하여 해결하였다.
아직도 진행중인데 epoch를 5번이나 돌린걸 후회한다.
3. 모델 테스트하기
# 입력 데이터 변환
def convert_input_data(sentences):
# BERT의 토크나이저로 문장을 토큰으로 분리
tokenized_texts = [tokenizer.tokenize(sent) for sent in sentences]
# 입력 토큰의 최대 시퀀스 길이
MAX_LEN = 128
# 토큰을 숫자 인덱스로 변환
input_ids = [tokenizer.convert_tokens_to_ids(x) for x in tokenized_texts]
# 문장을 MAX_LEN 길이에 맞게 자르고, 모자란 부분을 패딩 0으로 채움
input_ids = pad_sequences(input_ids, maxlen=MAX_LEN, dtype="long", truncating="post", padding="post")
# 어텐션 마스크 초기화
attention_masks = []
# 어텐션 마스크를 패딩이 아니면 1, 패딩이면 0으로 설정
# 패딩 부분은 BERT 모델에서 어텐션을 수행하지 않아 속도 향상
for seq in input_ids:
seq_mask = [float(i>0) for i in seq]
attention_masks.append(seq_mask)
# 데이터를 파이토치의 텐서로 변환
inputs = torch.tensor(input_ids)
masks = torch.tensor(attention_masks)
return inputs, masks
# 문장 테스트
def test_sentences(sentences):
# 평가모드로 변경
model.eval()
# 문장을 입력 데이터로 변환
inputs, masks = convert_input_data(sentences)
# 데이터를 GPU에 넣음
b_input_ids = inputs.to(device)
b_input_mask = masks.to(device)
# 그래디언트 계산 안함
with torch.no_grad():
# Forward 수행
outputs = model(b_input_ids,
token_type_ids=None,
attention_mask=b_input_mask)
# 로스 구함
logits = outputs[0]
# CPU로 데이터 이동
logits = logits.detach().cpu().numpy()
return logits
logits = test_sentences(['더 나은 학교생활 하고 싶어'])
print(logits)
'개발 > AI 코드' 카테고리의 다른 글
YOLO V8 detection 간단하게 사용하기 (0) | 2023.07.06 |
---|---|
인공지능 koBERT 모델 학습 (3) | 2023.02.11 |
댓글