AI를 활용한 창원시 치안지도 서비스 - (1) 치안기사분류 (BERT)

Vincent·2023년 2월 10일
0

Tools

  • Google colab
  • BERT

주요 코드 분석

참고한 BERT 사용법 :
[파이썬]일상/연애 주제의 한국어 대화 'BERT'로 이진 분류 모델 만들기 - 코드

학습데이터 불러오기

모델 학습을 위한 뉴스데이터 크롤링은 '경남신문'과 '아시아경제' 웹사이트에서 실시하였다.
일반적으로 뉴스 사이트에서 치안 관련 카테고리를 따로 만들어두고 있지 않기 때문에
뉴스 사이트 검색창에 '치안'을 검색하여 크롤링한 뉴스 데이터들을 치안 관련 뉴스라 하기로 하였다.

#필요한 라이브러리 불러오기
import kss
import os
import csv
import pandas as pd
import re
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 sklearn.model_selection import train_test_split
import numpy as np
import random
import time
import datetime

이제 BERT 모델이 치안 관련 문장을 참으로, 일반 문장을 거짓으로 학습하도록 하여
치안 관련 뉴스와 치안 관련 뉴스가 아닌 것을 분류할 수 있도록,
치안 뉴스가 들어있는 문장에는 1을, 일반 기사에 들어있는 문장에는 0을 라벨링한다.

#치안데이터 불러오기
chian_data = pd.read_csv('chian2021-08-23-15-19-08-경남신문/2021-08-23-15-19-08-경남신문.csv',
encoding="utf-8")

#뉴스 내용 시작과 끝에 있는 대괄호 제거
for i in range(0,len(chian_data)):
  chian_data['뉴스내용'][i] = re.sub("[\[\]]","",chian_data['뉴스내용'][i])
  
#문장단위 토크나이징
chian_sentences  = []
for i in range(0,len(chian_data)):
  chian_sentences.append(kss.split_sentences(chian_data['뉴스내용'][i]))
  
#리스트 내 리스트 합치기
chian_sentences = sum(chian_sentences , [])

#치안데이터 라벨링(True)
chian = pd.DataFrame(chian_sentences)
chian['label'] = 0 #초기화
chian.loc[(chian['label'] == 0), 'label'] = 1
chian = chian.rename(columns={0:'문장'})
chian.to_csv('치안뉴스라벨링.csv')

일반 뉴스가 아닌 카테고리로는 일반적으로 뉴스 사이트에서 나뉘어져 있는
정치, 경제, 연예, 스포츠, 문화, 기술을 선정했다.
아래의 코드는 문화 뉴스 데이터를 라벨링하는 과정이다.

#문화 데이터 불러오기
culture_data = pd.read_csv('culture2021-08-23-15-38-42-경남신문/2021-08-23-15-38-42-경남신문.csv',encoding="utf-8")
#뉴스 내용 시작과 끝에 있는 대괄호 제거
for i in range(0,len(culture_data)):
  culture_data['뉴스내용'][i] = re.sub("[\[\]]","",culture_data['뉴스내용'][i])
#문장단위 토크나이징
culture_sentences  = []
for i in range(0,len(culture_data)):
  culture_sentences.append(kss.split_sentences(culture_data['뉴스내용'][i]))
#리스트 내 리스트 합치기
culture_sentences = sum(culture_sentences , [])
#문화데이터 라벨링(False)
culture = pd.DataFrame(culture_sentences)
culture['label'] = 0


나머지 일반 카테고리들도 동일하게 진행해 만들어진 데이터프레임들을 연결한다.

notchian = pd.concat([politics,economy,entertainment,sports,culture,technology])
notchian = notchian.rename(columns={0:'문장'})
notchian.to_csv('일반뉴스라벨링.csv')

이제 본격적으로 BERT에 학습시키기 위해, 라벨링된 치안뉴스 & 일반뉴스 데이터프레임을 묶는다.

#치안, 일반 뉴스데이터 병합
data = pd.concat([chian,notchian])
data.to_csv('뉴스데이터셋.csv')
news = pd.read_csv('뉴스데이터셋.csv')
news.drop('Unnamed: 0',axis=1,inplace=True)

총 문장은 약 1800개이다.

Train set /Test set 나누기

특정 비율로 데이터셋을 나눌 때, 클래스가 '0'인 데이터와 '1'인 데이터가 골고루 들어가기 위해선 데이터를 랜덤으로 섞어주고 나눠야 한다. 따라서 다음과 같이 코드를 입력해준다.

news_shuffled = news.sample(frac=1).reset_index(drop=True)

#train data와 test data의 비율을 설정 (약 75:25)
#train data & test data 로드 
train = news_shuffled[:1300]
test = news_shuffled[1300:]

Train set 전처리

여기서부터 지금까지 전처리한 dataset이 BERT 모델의 입력데이터가 되기 위한 특수한 전처리가 필요하다.

  • (1) [CLS]와 [SEP]
    BERT 분류 모델의 경우 각 문장의 앞마다 [CLS]를 붙여 인식시키고, 문장의 종료는 [SEP]를 붙여 인식시킨다. [CLS]을 인식함으로써 문장의 처음이라 알 수 있게 하고, [SEP]을 인식함으로써 문장의 끝을 알 수 있게 하기 위함이다.
# CLS, SEP 붙이기 (문장의 시작, 끝)
sentences = ["[CLS] " + str(s) + " [SEP]" for s in train.문장]

한편, '0'과 '1'의 라벨이 들어있는 컬럼을 'labels'이라는 array에 따로 저장한다.

labels = train['label'].values
  • (2) 서브워드 토크나이저 : WordPiece
    단어를 토큰화 할 때, 단어집합에 없는 단어는 더 쪼개서 '##'을 붙여주는 방식
    토큰화 : AI 모델이 이해하고 처리할 수 있는 형태로 변환
tokenizer = BertTokenizer.from_pretrained('bert-base-multilingual-cased', do_lower_case=False)
tokenized_texts = [tokenizer.tokenize(s) for s in sentences]

이 이후에, 문장의 최대 시퀀스를 설정하여 정수 인코딩과 제로 패딩을 수행해준다.

패딩 : 출력 데이터의 크기를 조정할 목적으로 사용

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")
  • (3) 어텐션 마스크
    0 값을 가지는 패딩 토큰에 대해서 어텐션 연산을 불필요하게 수행하지 않도록 단어와 패딩 토큰을 구분할 수 있게 알려주는 것
    어텐션 : 입력 시퀀스에 대해 가중치를 부여하여 특정 단어나 구문을 강조하거나 중요성을 부여. 출력에 영향을 주는 입력 요소를 선택적으로 인식.
#예) 패딩된 데이터 = [40311, 9435, 102, 0, 0]
#>>어텐션 마스크 = [1.0, 1.0, 1.0, 0.0, 0.0]
attention_masks = []

for seq in input_ids:
    seq_mask = [float(i>0) for i in seq]
    attention_masks.append(seq_mask)
  • (4) Train set을 훈련셋과 검증셋으로 분리하기
    데이터가 입력의 형태를 갖췄다면 훈련셋과 검증셋으로 분리해주도록 한다. 어텐션 마스크도 함께 훈련셋과 검증셋으로 분리하고, 데이터를 모두 파이토치 텐서로 변환시킨다.
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)         

마지막으로 배치사이즈를 32로 설정하고, 입력데이터, 어텐션 마스크, 라벨을 하나의 데이터로 묶어 train_dataloader, validation_dataloader라는 입력데이터를 생성해준다.

batch_size = 32

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)

Test set 전처리

train set 전처리와 동일

# [CLS] + 문장 + [SEP]
sentences = ["[CLS] " + str(sentence) + " [SEP]" for sentence in test.문장]

# 라벨 데이터
labels = test['label'].values

# Word 토크나이저 토큰화
tokenizer = BertTokenizer.from_pretrained('bert-base-multilingual-cased', do_lower_case=False)
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]
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)
    
# 파이토치 텐서로 변환
test_inputs = torch.tensor(input_ids)
test_labels = torch.tensor(labels)
test_masks = torch.tensor(attention_masks)

# 배치 사이즈 설정 및 데이터 설정
batch_size = 32
test_data = TensorDataset(test_inputs, test_masks, test_labels)
test_sampler = RandomSampler(test_data)
test_dataloader = DataLoader(test_data, sampler=test_sampler, batch_size=batch_size)

BERT 모델 불러오기

model = BertForSequenceClassification.from_pretrained("bert-base-multilingual-cased", num_labels=2)
model

한편, 하이퍼 파라미터를 설정해주어야 하는데, 파라미터 값은 참조한 코드에 있는 값 그대로 설정했다. 에폭 수는 간단하게 100으로 설정해주었다.

# 옵티마이저
optimizer = AdamW(model.parameters(),
                  lr = 2e-5, # 학습률(learning rate)
                  eps = 1e-8 
                )

# 에폭수
epochs = 100

# 총 훈련 스텝 : 배치반복 횟수 * 에폭
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))
#랜덤시드 고정
seed_val = 42
random.seed(seed_val)
np.random.seed(seed_val)
torch.manual_seed(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

        # Forward 수행                
        outputs = model(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!")

테스트셋 평가

#시작 시간 설정
t0 = time.time()

# 평가모드로 변경
model.eval()

# 변수 초기화
eval_loss, eval_accuracy = 0, 0
nb_eval_steps, nb_eval_examples = 0, 0

# 데이터로더에서 배치만큼 반복하여 가져옴
for step, batch in enumerate(test_dataloader):
    # 경과 정보 표시
    if step % 100 == 0 and not step == 0:
        elapsed = format_time(time.time() - t0)
        print('  Batch {:>5,}  of  {:>5,}.    Elapsed: {:}.'.format(step, len(test_dataloader), elapsed))

    # 배치를 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("")
print("Accuracy: {0:.2f}".format(eval_accuracy/nb_eval_steps))
print("Test took: {:}".format(format_time(time.time() - t0)))

새로운 문장 테스트

입력하는 문장도 BERT의 입력 형태로 만드는 작업이 필요하기 때문에 우선 아래 코드를 실행해준다.

# 입력 데이터 변환
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(["""(서울=연합뉴스) 김승욱 기자 = 경찰이 진화하는 사이버 범죄에 대응하고자 관련 수사를 강화한다.

20일 연합뉴스 취재에 따르면 경찰청은 전국 18개 시도경찰청 중 11곳에만 있는 과 단위의 사이버수사 조직을 18곳 모두에 설치하는 방안을 검토하고 있다.

아울러 사이버수사과 산하에 사이버미제수사팀을 신설하는 방안도 추진 중이다.


경찰 관계자는 "사이버 범죄가 갈수록 고도화하고 피해도 늘어 수사를 강화하려는 것"이라며 "신설되는 조직에 투입할 인력 규모 등을 협의 중"이라고 말했다.


시대 변화로 생활 공간이 온라인으로 이동하면서 사이버 범죄도 급증하는 추세다.

경찰청은 올해 2월부터 6월까지 5개월간 사기 범죄를 단속해 2만9천881명을 검거했는데, 범죄 유형별로는 사이버 사기가 1만2천84명으로 가장 많았다.

사이버 공간에서는 'n번방'이나 '박사방' 사건처럼 미성년자를 상대로 한 성범죄도 적지 않아 단속 강화 필요성이 꾸준히 제기됐다."""])
print(logits)

if np.argmax(logits) == 1 :
    print("치안 뉴스")
elif np.argmax(logits) == 0 :
    print("일반 뉴스")

이 분류모델을 활용하여 '경남신문'과 '아시아경제' 웹사이트에서 '창원'을 키워드로 하여 크롤링한 기사들 중 치안관련뉴스들만 선별하는 작업을 실시하였다.

profile
Frontend & Artificial Intelligence

0개의 댓글