# KoBERT + Sphere 게시판 classification ### 1. Elastic Cloud에서 데이터 추출 Elastic Cloud의 Kibana에서 모든 document를 export하는 방법을 찾지 못해 Elastic Search API를 활용해서 게시판 모든 데이터를 .json형식으로 추출했다. ![](https://i.imgur.com/SrRIvAF.png) [링크](https://apipheny.io/import-json-google-sheets/)를 참조하여 json to excel로 변환하고 colab에서 작업하기 쉽게 google sheet로 저장했다. ### 2. 데이터 라벨링 총 800건 정도 되는 글을 카테고리 별로 라벨링하는데 꾀 많은 시간이 필요했다. 라벨링을 진행하면서 중간중간 카테고리를 수정해야하는 상황도 발생해서 시간을 더 지채했다. ![](https://i.imgur.com/OfI6aVP.png) 데이터 양에 비해 라벨종류가 너무 많은게 아닌가 싶었지만, 일단 첫 시도때 라벨은 다음과 같이 정의했다. ``` 소통: 운영자의 답변이 필요없는 커뮤니티성 글 후기: 이벤트 후기 공지: 운영자 공지 소음: 소음관련 문의 네트워크: 인터넷 연결관련 문의 시설: 시설관련 VoC (예: 모니터가 안되요, 화장실 문 고장났어요, 너무 더워요) 예약: 회의실 & 좌석 예약 관련 (월패드, 키오스크, 모바일 포함) 이벤트: 이벤트 관련 문의 주차: 주차장 관련 문의 분실물: 분실물 관련 문의 기타건의: 기타 건의 or 문의 삭제: 테스트성 글 (train 데이터에서 제외) ``` ### 3. Dataset 만들기 Googlesheet를 dataframe으로 변환 후 필요한 컬럼만 추출하고 테스트성 글들은 제거했다. ```python # 구글 시트 연결 from google.colab import auth import gspread from google.auth import default auth.authenticate_user() creds, _ = default() gc = gspread.authorize(creds) # 구글 시트를 dataframe으로 변환 import pandas as pd def gsheet_to_dataframe(gsheet_name): worksheet = gc.open(gsheet_name).sheet1 rows = worksheet.get_all_values() df = pd.DataFrame(rows) df.columns = df.iloc[0] df = df.iloc[1:] df = df.filter(items=["title", "content", "category"]) df = df[(df.category != "삭제")] return df ``` ![](https://i.imgur.com/pxKgwFx.png) KoBERT의 vocab 리스트를 가져와서 데이터를 BertDataset으로 변환해주었는데, 게시글에서 500개는 train 데이터로 나머지는 test 데이터로 분리해서 dataset를 만들었다. ```python class BERTDataset(Dataset): def __init__(self, dataset, sent_idx, label_idx, bert_tokenizer, max_len, pad, pair): transform = nlp.data.BERTSentenceTransform( bert_tokenizer, max_seq_length=max_len, pad=pad, pair=pair) self.sentences = [transform([i[sent_idx]]) for i in dataset] self.labels = [np.int32(i[label_idx]) for i in dataset] def __getitem__(self, i): return (self.sentences[i] + (self.labels[i], )) def __len__(self): return (len(self.labels)) ``` title과 content를 concat해서 sentence 필드로 정의하고 category 컬럼값은 factorize로 int로 변환해서 label이란 필드로 정의했다. ```python df['sentence'] = df['title'] + ' ' + df['content'] df['label'] = pd.factorize(df['category'])[0] dataset_train = df.values[:500] dataset_test = df.values[500:] vocab = get_pytorch_kobert_model() tokenizer = get_tokenizer() tok = nlp.data.BERTSPTokenizer(tokenizer, vocab, lower=False) # BERTDataset 클래스 이용, TensorDataset으로 만들어주기 3번째 index에 sentence가 있고 4번째 index에 label값이 있다. data_train = BERTDataset(dataset_train, 3, 4, tok, max_len, True, False) data_test = BERTDataset(dataset_test, 3, 4, tok, max_len, True, False) ``` ![](https://i.imgur.com/a08u2ls.png) ### 4. 모델 학습시키기 SKTBrain이 기본으로 제공하는 BERT classifier ```python class BERTClassifier(nn.Module): def __init__(self, bert, hidden_size = 768, num_classes= 11, # 시설, 소통, 공지, 소음, 분실물, 네트워크, 예약, 이벤트, 기타건의, 주차, 후기 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) ``` ```python # 만약 gpu 사용이 가능하다면 device = torch.device("cuda:0") model = BERTClassifier(bertmodel, dr_rate=0.5).to(device) # 불가능하면 model = BERTClassifier(bertmodel, dr_rate=0.5) ``` Optimizer와 Scheduler ```python no_decay = ['bias', 'LayerNorm.weight'] optimizer_grouped_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_grouped_parameters, lr=learning_rate) loss_fn = nn.CrossEntropyLoss() t_total = len(train_dataloader) * num_epochs warmup_step = int(t_total * warmup_ratio) scheduler = get_cosine_schedule_with_warmup(optimizer, num_warmup_steps=warmup_step, num_training_steps=t_total) 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 ``` 아래 스크립트에서 gpu 사용이 가능하면 .to(device)를 모두 붙여준다. ```python for e in range(num_epochs): train_acc = 0.0 test_acc = 0.0 model.train() for batch_id, (token_ids, valid_length, segment_ids, label) in enumerate(tqdm_notebook(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() torch.nn.utils.clip_grad_norm_(model.parameters(), max_grad_norm) optimizer.step() scheduler.step() # Update learning rate schedule train_acc += calc_accuracy(out, label) if batch_id % log_interval == 0: print("epoch {} batch id {} loss {} train acc {}".format(e+1, batch_id+1, loss.data.cpu().numpy(), train_acc / (batch_id+1))) 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 enumerate(tqdm_notebook(test_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) test_acc += calc_accuracy(out, label) print("epoch {} validation acc {}".format(e+1, test_acc / (batch_id+1))) ``` 필자는 colab에서 gpu사용량이 초과해서 cpu로 돌렸더니 한세월 걸린다. ![](https://i.imgur.com/yrBAw73.png)