1. 프로젝트 정의
비속어 [단어] 탐지 모델을 만들어보려고 하는데 가지고 있는 데이터가 적기 때문에 BERT 기반으로 비속어 감지 모델을 구축하고자 한다. 이때 transformers 라이브러리로 pre-trained BERT 모델을 불러오고, 가지고 있는 데이터로 fine-tuning하려고 한다. 모델을 활용해 주어진 단어가 비속어인지 아닌지 여부를 예측할 수 있다.
2. 데이터
https://jizard.tistory.com/288
자주쓰는 최신 비속어 리스트 (욕설 필터링 txt 첨부파일 제공)
인터넷에 떠돌아다니는 비속어 리스트를 보면...적어도 20년은 묵은듯한 단어들이 많이 나오는데, 이건 그래도 비교적 최신(?) 비속어 리스트다.
jizard.tistory.com
위의 사이트에서 "fword_list.txt" 를 다운받아 비속어 데이터로 사용했으며, 일반 단어는 아래 사이트에서 "한국어+학습용+어휘+목록.csv" 를 다운받아 사용했다.
3beol.github.io/한국어+학습용+어휘+목록.csv at master · 3beol/3beol.github.io
이사갑니다. Contribute to 3beol/3beol.github.io development by creating an account on GitHub.
github.com
3. 데이터 전처리
1) 데이터에 라벨 추가
각 데이터에 라벨이 없기 때문에 "fword_list.txt" 데이터에는 label 에 1을 , "한국어+학습용+어휘+목록.csv" 데이터에는 label 에 0을 추가해주었다. 그 전에 일반 단어 데이터에 "가구1" 과 같이 문자와 숫자가 섞여 있어서 숫자를 제거해주는 코드를 먼저 실행했다.
import pandas as pd
import re
# 1. 데이터셋 불러오기
data = pd.read_table('./fword_list.txt', names=['word'])
good_data = pd.read_csv('https://raw.githubusercontent.com/3beol/3beol.github.io/refs/heads/master/%ED%95%9C%EA%B5%AD%EC%96%B4%2B%ED%95%99%EC%8A%B5%EC%9A%A9%2B%EC%96%B4%ED%9C%98%2B%EB%AA%A9%EB%A1%9D.csv')
# 2. good_data에 숫자 제거해주기
good_data = good_data.values.tolist( )
g_data = sum(good_data, [])
g_data_lst = list()
for i in g_data:
tmp = re.sub(r'\d', '', i)
g_data_lst.append(tmp)
g_data_df = pd.DataFrame(g_data_lst, columns=['word'])
# 3. 라벨 추가하기
data['label'] = 1
g_data_df['label'] = 0
# 4. 데이터 병합
data_ = pd.concat([data, g_data_df], axis=0)
# 5. 데이터 중복 제거
data_.drop_duplicates(subset=['word'], keep='first', inplace=True, ignore_index=True)
# 6. max_length 찾기
max_length = data_['word'].str.len().max()
2) 데이터 shuffle 뒤 데이터셋 분리
# 7. 데이터 shuffle
data_shuffle = data_.sample(frac=1).reset_index(drop=True)
# 8. 데이터 셋 분리
train_texts, test_texts, train_labels, test_labels = train_test_split(data_shuffle['word'].tolist(), data_shuffle['label'].tolist(), test_size=0.2)
4. 학습 진행
Bert 토크나이저를 실행하고, 데이터셋을 만들어준 뒤 모델 훈련을 진행한다.
코랩 T4 GPU 환경에서 실행했으며, 진행시 wandb.ai 의 API key를 입력해야 한다. 시간이 25분 좀 넘게 걸렸다.
Sign In with Auth0
wandb.ai
들어가서 가입하면 API key를 얻을 수 있었다.
# 9. 토크나이저 로드
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
# 10. 토큰화 및 데이터셋 클래스 정의
class CustomDataset(Dataset):
# text와 label을 입력받고, 토크나이저를 사용하여 토큰화 진행
def __init__(self, texts, labels):
# truncation : 문장의 길이가 지정된 길이보다 길면 잘라냄 (max_length)
# padding : 짧은 문장은 패딩으로 길이를 동일하게 맞춤
# encodings 에 토큰화된 텍스트가 저장됨
self.encodings = tokenizer(texts, truncation=True, padding=True, max_length=5)
self.labels = labels
def __getitem__(self, idx):
# 토큰화된 텍스트의 딕셔너리를 텐서형태로 변환
item = {key: torch.tensor(val[idx]) for key, val in self.encodings.items()}
item['labels'] = torch.tensor(self.labels[idx])
return item
def __len__(self):
return len(self.labels)
train_dataset = CustomDataset(train_texts, train_labels)
test_dataset = CustomDataset(test_texts, test_labels)
# 11. BERT 모델 로드
model = BertForSequenceClassification.from_pretrained('bert-base-uncased', num_labels=2)
# 12. 훈련 설정
training_args = TrainingArguments(
output_dir='./results', # 결과 저장 경로
evaluation_strategy="epoch", # 에폭마다 평가
per_device_train_batch_size=16, # 배치 크기
per_device_eval_batch_size=16,
num_train_epochs=50, # 학습 에폭 수
weight_decay=0.001, # 가중치 감쇠 (regularization)
)
# 13. Trainer 설정
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_dataset,
eval_dataset=test_dataset,
)
# 14. 모델 학습
trainer.train()
# 15. 모델 평가
trainer.evaluate()
이 때, 학습을 여러번 진행하면서 테스트하다보니 results 폴더에 쌓이는데, 새로 학습할 때 마다 비워주고 싶었다.
근데 코랩에서 그냥 폴더 삭제를 진행하면 비어있지 않은 폴더는 삭제가 되지 않아 다음과 같은 방법을 사용했다.
import shutil
shutil.rmtree('./results')
5. 평가 및 테스트 진행
테스트 진행시 욕을 넣어서 실행해보려고 했지만 아래와 같은 오류가 발생했다.
# 16. 새 문장으로 예측
def predict(text):
inputs = tokenizer(text, return_tensors="pt", truncation=True, padding=True, max_length=5)
outputs = model(**inputs)
logits = outputs.logits
prediction = torch.argmax(logits, dim=-1)
return "비속어" if prediction == 1 else "비속어 아님"
# 예측 테스트
print(predict("**"))
모델과 입력 데이터[텐서] 서로 다른 디바이스에 할당된 경우에 나타나는 오류로, 일부 텐서는 GPU[cuda] 에 있고, 다른 텐서는 CPU에 있다는 뜻이다. 그래서 아래와 같이 모델 디바이스 확인해 준 뒤, input 의 텐서를 모두 gpu 로 이동 시켰다.
# 모델 디바이스 확인
print(model.device)
def predict(text):
inputs = tokenizer(text, return_tensors="pt", truncation=True, padding=True, max_length=128)
# 모든 텐서를 GPU로 이동
inputs = {key: value.to('cuda') for key, value in inputs.items()}
outputs = model(**inputs)
logits = outputs.logits
prediction = torch.argmax(logits, dim=-1)
return "비속어" if prediction == 1 else "비속어 아님"
그랬더니 결과는 아래와 같았다.
너무 심한 욕인가 싶었지만 일단 대표적인 욕으로 확인 완료.
6. 변주
몇가지 변주를 진행해봤는데,
1. 당연스럽게도 g_data 없이 fword 데이터로만 학습 시키면 모든 단어가 비속어로 나온다.
2. g_data 를 fword 데이터의 수만큼 줄이고 [언더 샘플링] 학습을 시키면 비속어를 더 잘 탐지하지만 일반 단어도 비속어로 탐지하는 경향이 있다.