├── README.md ├── app ├── api │ ├── hate_speech_binary │ │ ├── Dockerfile │ │ ├── app.py │ │ ├── environment.yml │ │ └── model │ │ │ └── README.md │ └── hate_speech_topic │ │ ├── Dockerfile │ │ ├── app.py │ │ ├── environment.yml │ │ └── model │ │ └── README.md └── hate_speech_main │ ├── Dockerfile │ ├── app.py │ ├── environment.yml │ ├── static │ ├── css │ │ ├── index.css │ │ ├── loading.css │ │ ├── page.css │ │ └── results.css │ ├── fonts │ │ └── NanumSquareB.ttf │ └── images │ │ ├── foreign.png │ │ ├── gender.png │ │ ├── political.png │ │ └── regional.png │ └── templates │ ├── index.html │ ├── input_page.html │ ├── loading.html │ ├── results.html │ └── scrape_page.html └── modeling ├── dataset └── README.md ├── model └── README.md └── modeling.ipynb /README.md: -------------------------------------------------------------------------------- 1 | # hate-speech-language-modeling 2 | Recurrent Neural Network based Hate Speech Language Model for Korean Hate Speech Detection 3 |
- by 류병우 4 | 5 | web service available (try it yourself!!!): https://hate-speech-main-c2eedpqzcq-an.a.run.app 6 |
demo video: https://www.youtube.com/watch?v=HnhS6BgmcDg 7 |
the models and datasets are available at https://www.kaggle.com/captainnemo9292/korean-hate-speech-dataset 8 | 9 | 안녕하세요 Github, 딥러닝에 관심있는 고등학생입니다. 이번에 발생한 n번방 사태에 대해 큰 충격을 받고, 우리나라의 혐오 문화를 AI로 접근할 방법이 있을까 생각하다가 인공지능 기반 혐오 발언 탐지 웹서비스를 구현해보았습니다. 극우 사이트 일간베스트의 댓글들을 학습 데이터로써 크롤링하여 혐오 발언 이진 분류 RNN 모델을 개발했고, NMF 토픽 모델 알고리즘을 활용하여 혐오 발언의 주제를 추출하여 multi-class classification 언어 모델을 학습시켰습니다. ( 주제 1 - 특정 지역에 대한 차별, 주제 2 - 정치적 성향이 다른 사람들에 대한 왜곡, 주제 3 - 다른 나라에 대한 차별, 주제 4 - 여성 차별 및 성적 발언) 한 번씩 봐주시면 감사하겠습니다. 다음은 제 토이프로젝트의 과정입니다. 10 | 11 | 1. 웹크롤링을 통한 데이터 수집 12 | 13 | 극우 사이트인 ‘일간베스트‘의 댓글들은 거의 대 부분 혐오 게시물에 대한 답글이고 다수의 혐오 표현을 포함하기에 언어 모델링에 필요한 혐오 발언 데이터로써 적합하다고 생각했습니다. 따 라서 Selenium 모듈을 활용해 인기 포스트의 댓글 약 10만개를 크롤링했습니다. 14 | 15 | 2. 텍스트 전처리 16 | 17 | Tokenization: 혐오 및 왜곡 발언은 주로 명사의 형태를 띠는 혐오 표현을 포함하고 있기에 명사 단위의 토큰화가 적절하다고 생각했습니다. 따라서 한글 전용 NLP 전용 라이브러리, KoNLPY의 형태소추출 기능을 활용하여 명사 추출을 진행했습니다. 18 | 19 | 불용어 제거: 혐오 발언 텍스트에는 흔한 욕설이 자주 등 장하지만, 특정 집단에 대한 편견을 감지하는 것에 집중하고 싶었던 저는 그러한 비속어가 분석에 큰 도움이 되지 않을 것이라고 생각했습니다. 따라서, 평범한 욕설은 불용어로 취급하고 제거하는 과정을 거쳤습니다. 20 | 21 | 3. 토픽 모델링 22 | 23 | 우리 사회의 혐오 문화가 어떠한 사회적 약자에 대한 차별과 왜곡을 조장하고 있는지 분석하는 과정이 필요하다고 생각했습니다. 혐오 발언의 차별 대상을 각각 특정한 주제로 취급하여 토픽 모델링을 적용하면 사회적 약자에 대한 왜곡된 인식을 밝혀낼 수 있을 것이라 생각했습니다. 24 | 25 | Tf-idf 벡터화: 모든 문서에서 자주 등장하는 평범한 비속어에는 낮은 가중치를, 특정한 문서에서만 자주 등장하는 혐오 표현에는 높은 가중치를 부여하기 위해 Tf-idf 벡터화를 활용했습니다. 26 | 27 | 토픽 모델링: 이후 Tf-idf 벡터화된 데이터에서 NMF 알고리즘을 활용해 주제를 추출했습니다. 토픽 모델링 결과, (Topic 1) 특정 지역에 대한 차별적 발언, (Topic 2) 정치적 성향이 다른 사람들에 대한 왜곡, (Topic 3) 외국에 대한 차별적 발언, (Topic 4) 여성에 대한 혐오 및 성적 발언; 왼쪽 그림과 같이 4가지 주제를 추출할 수 있었습니다. 28 | 29 | 4. 언어 모델링 30 | 31 | 혐오 발언 댓글을 네이버영화 리뷰 데이터셋과 섞어서 구성한 이진 분류 데이터셋과 혐오 표현을 주제별로 분류한 토픽 데이터셋을 각각 RNN 인공신경망에 훈련시켜 이진 분류 모델과 multi-class classification 모델을 개발했습니다. Multilingual BERT에도 전이 학습 시켜보았으나, 과도한 메모리 사용으로 인해 웹서비스에는 배포하지 못했습니다. 32 | 33 | 5. 인공지능 기반 혐오표현 감지 웹서비스 34 | 35 | Flask 웹 프레임워크를 활용해 백엔드를 구축하고 인터넷에 웹서비스로써 배포하였습니다. 인공지능 기반 혐오표현 감지 웹서비스의 구성은 매우 간단합니다. 사용자가 온라인에서 혐오 발언으로 의심되는 혐오 표현을 마주했을 때 웹 상에서 서비스를 손쉽게 이용할 수 있습니다. 글을 직접 작성하면 API로 배포되어 있는 혐오 발언 이진 분류 모델을 활용하여 혐오 표현인지 여부를 판단합니다. 인공지능이 혐오 표현이라고 판단한 문장들은 또다른 API로 배포되어 있는 혐오 발언 주제 분류 모델을 활용하여 위에 제시된 4가지 주제 중 어느 차별적 사상을 담고 있는지 분석합니다. 분석 결과 페이지에서 혐오 발언 문장과 각각의 차별 대상 데이터 표를 한눈에 파악할 수 있습니다. 글을 직접 입력하지 않더라도 의심되는 표현이 제시된 웹페이지의 URL 주소를 입력하면 웹 어플리케이션이 직접 해당 페이지의 텍스트를 크롤링하여 분석합니다. 위에 프로토타입 링크가 제시되어 있어 직접 사용해 볼 수 있고 데모 영상을 통해 작동 과정을 파악할 수 있습니다. 36 | 37 | p.s. 본래 BERT에다 전이 학습 시켜서 성능이 훨씬 좋았는데, 메모리를 너무 많이 잡아먹어 웹 서비스가 다운된다는 문제게 생기기 때문에 간단한 RNN으로 대체했습니다. 따라서 성능이 좋지 않다고 보실 수도 있습니다. 제가 전문가가 아니라는 점 가만해 주십시오 38 | -------------------------------------------------------------------------------- /app/api/hate_speech_binary/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM continuumio/miniconda3 2 | 3 | ENV APP_HOME /app 4 | 5 | WORKDIR $APP_HOME 6 | 7 | COPY . . 8 | RUN conda env create -f environment.yml 9 | 10 | SHELL ["conda", "run", "-n", "hate_env", "/bin/bash", "-c"] 11 | 12 | # Make sure the environment is activated: 13 | RUN echo "Make sure flask is installed:" 14 | RUN python -c "import flask" 15 | 16 | ENTRYPOINT ["conda", "run", "-n", "hate_env", "python", "app.py"] 17 | -------------------------------------------------------------------------------- /app/api/hate_speech_binary/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, jsonify 2 | from keras.preprocessing import sequence 3 | from keras.preprocessing.text import tokenizer_from_json 4 | from keras.models import load_model 5 | import json 6 | import sys 7 | import os 8 | 9 | app = Flask(__name__) 10 | 11 | 12 | def load_pretrained(): 13 | model = load_model('./model/model.h5') 14 | with open('./model/tokenizer.json') as f: 15 | data = json.load(f) 16 | tokenizer = tokenizer_from_json(data) 17 | print('loaded') 18 | sys.stdout.flush() 19 | return model, tokenizer 20 | 21 | 22 | def get_prediction(input_sentences): 23 | model, tokenizer = load_pretrained() 24 | classes = ["혐오", "정상"] 25 | labels = [] 26 | predict = model.predict(sequence.pad_sequences(tokenizer.texts_to_sequences(input_sentences), maxlen=128)) 27 | for i, pred in enumerate(predict): 28 | print(pred) 29 | sys.stdout.flush() 30 | if pred[0]>=0.99: 31 | labels.append({"sentence" : input_sentences[i], "label" : classes[0]}) 32 | else: 33 | labels.append({"sentence" : input_sentences[i],"label" : classes[1]}) 34 | return labels 35 | 36 | 37 | @app.route("/wake", methods=['POST']) 38 | def wake(): 39 | return jsonify({'wake': 'wake'}) 40 | 41 | 42 | @app.route("/predict", methods=['POST']) 43 | def predict(): 44 | if request.method == 'POST': 45 | req_data = request.json 46 | print(req_data) 47 | sys.stdout.flush() 48 | input_sentences = req_data['input_sentences'] 49 | labels = get_prediction(input_sentences) 50 | print(labels) 51 | sys.stdout.flush() 52 | return jsonify({'predictions': labels}) 53 | 54 | 55 | if __name__ == "__main__": 56 | app.run(debug=True, host='0.0.0.0', port=int(os.environ.get('PORT', 8080))) 57 | -------------------------------------------------------------------------------- /app/api/hate_speech_binary/environment.yml: -------------------------------------------------------------------------------- 1 | name: hate_env 2 | dependencies: 3 | - nomkl 4 | - python=3.7 5 | - pip 6 | - pip: 7 | - Flask==1.1.2 8 | - gunicorn==20.0.4 9 | - grpcio==1.27.2 10 | - h5py==2.10.0 11 | - Jinja2==2.11.1 12 | - Keras==2.3.1 13 | - Keras-Applications==1.0.8 14 | - Keras-Preprocessing==1.1.0 15 | - numpy==1.18.1 16 | - requests==2.23.0 17 | - tensorboard==1.14.0 18 | - tensorflow==1.14.0 19 | - tensorflow-estimator==1.14.0 20 | - urllib3==1.25.8 21 | - Werkzeug==1.0.0 22 | -------------------------------------------------------------------------------- /app/api/hate_speech_binary/model/README.md: -------------------------------------------------------------------------------- 1 | # hate-speech-language-modeling 2 | the models are available at https://www.kaggle.com/captainnemo9292/korean-hate-speech-dataset 3 | -------------------------------------------------------------------------------- /app/api/hate_speech_topic/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM continuumio/miniconda3 2 | 3 | ENV APP_HOME /app 4 | 5 | WORKDIR $APP_HOME 6 | 7 | COPY . . 8 | RUN conda env create -f environment.yml 9 | 10 | SHELL ["conda", "run", "-n", "hate_env", "/bin/bash", "-c"] 11 | 12 | # Make sure the environment is activated: 13 | RUN echo "Make sure flask is installed:" 14 | RUN python -c "import flask" 15 | 16 | ENTRYPOINT ["conda", "run", "-n", "hate_env", "python", "app.py"] 17 | -------------------------------------------------------------------------------- /app/api/hate_speech_topic/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, jsonify 2 | from keras.preprocessing import sequence 3 | from keras.preprocessing.text import tokenizer_from_json 4 | from keras.models import load_model 5 | import json 6 | import sys 7 | import os 8 | 9 | app = Flask(__name__) 10 | 11 | 12 | def load_pretrained(): 13 | model = load_model('./model/model.h5') 14 | with open('./model/tokenizer.json') as f: 15 | data = json.load(f) 16 | tokenizer = tokenizer_from_json(data) 17 | print('loaded') 18 | sys.stdout.flush() 19 | return model, tokenizer 20 | 21 | 22 | def get_prediction(input_sentences): 23 | model, tokenizer = load_pretrained() 24 | classes = ["특정 지역에 대한 차별적 발언", "정치적 성향이 다른 사람들에 대한 혐오 및 왜곡", "다른 나라에 대한 차별적 발언", "여성 및 성소수자에 대한 혐오 및 왜곡"] 25 | labels = [] 26 | predict = model.predict(sequence.pad_sequences(tokenizer.texts_to_sequences(input_sentences), maxlen=128)) 27 | for i, pred in enumerate(predict): 28 | labels.append({"sentence" : input_sentences[i], "label" : classes[pred.argmax()]}) 29 | return labels 30 | 31 | 32 | @app.route("/wake", methods=['POST']) 33 | def wake(): 34 | return jsonify({'wake': 'wake'}) 35 | 36 | 37 | @app.route("/predict", methods=['POST']) 38 | def predict(): 39 | if request.method == 'POST': 40 | req_data = request.json 41 | print(req_data) 42 | sys.stdout.flush() 43 | input_sentences = req_data['input_sentences'] 44 | labels = get_prediction(input_sentences) 45 | print(labels) 46 | sys.stdout.flush() 47 | return jsonify({'predictions': labels}) 48 | 49 | 50 | if __name__ == "__main__": 51 | app.run(debug=True, host='0.0.0.0', port=int(os.environ.get('PORT', 8080))) 52 | -------------------------------------------------------------------------------- /app/api/hate_speech_topic/environment.yml: -------------------------------------------------------------------------------- 1 | name: hate_env 2 | dependencies: 3 | - nomkl 4 | - python=3.7 5 | - pip 6 | - pip: 7 | - Flask==1.1.2 8 | - gunicorn==20.0.4 9 | - grpcio==1.27.2 10 | - h5py==2.10.0 11 | - Jinja2==2.11.1 12 | - Keras==2.3.1 13 | - Keras-Applications==1.0.8 14 | - Keras-Preprocessing==1.1.0 15 | - numpy==1.18.1 16 | - requests==2.23.0 17 | - tensorboard==1.14.0 18 | - tensorflow==1.14.0 19 | - tensorflow-estimator==1.14.0 20 | - urllib3==1.25.8 21 | - Werkzeug==1.0.0 22 | -------------------------------------------------------------------------------- /app/api/hate_speech_topic/model/README.md: -------------------------------------------------------------------------------- 1 | # hate-speech-language-modeling 2 | the models are available at https://www.kaggle.com/captainnemo9292/korean-hate-speech-dataset 3 | -------------------------------------------------------------------------------- /app/hate_speech_main/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM continuumio/miniconda3 2 | 3 | ENV APP_HOME /app 4 | 5 | WORKDIR $APP_HOME 6 | 7 | COPY . . 8 | RUN conda env create -f environment.yml 9 | 10 | SHELL ["conda", "run", "-n", "hate_env", "/bin/bash", "-c"] 11 | 12 | # Make sure the environment is activated: 13 | RUN echo "Make sure flask is installed:" 14 | RUN python -c "import flask" 15 | 16 | ENTRYPOINT ["conda", "run", "-n", "hate_env", "python", "app.py"] 17 | -------------------------------------------------------------------------------- /app/hate_speech_main/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, render_template 2 | import requests 3 | from newspaper import Article 4 | import sys 5 | from urllib.parse import urlencode 6 | import re 7 | import os 8 | 9 | app = Flask(__name__) 10 | 11 | 12 | def scrape_url(url): 13 | article = Article(url) 14 | article.download() 15 | article.parse() 16 | article_text = article.text 17 | """ 18 | if len(article_text)>1000: 19 | article_text = article_text[:1000] 20 | """ 21 | print(len(article_text)) 22 | sys.stdout.flush() 23 | return article_text 24 | 25 | 26 | def text_preprocessing(raw_corpus): 27 | raw_corpus = raw_corpus.replace('\n', '') 28 | raw_corpus = re.split('[.?!]', raw_corpus) 29 | sentence_list = [] 30 | for sentence in raw_corpus: 31 | if len(sentence)<=128: 32 | sentence_list.append(sentence) 33 | else: 34 | sentence_list.append(sentence[:128]) 35 | return sentence_list 36 | 37 | 38 | def analyze_binary(input_sentences): 39 | res = requests.post("https://hate-speech-binary-c2eedpqzcq-an.a.run.app/predict", json = {"input_sentences": input_sentences}) 40 | if res.status_code != 200: 41 | raise Exception("Error: API request unsuccessful.") 42 | data = res.json() 43 | return data['predictions'] 44 | 45 | 46 | def analyze_topic(input_sentences): 47 | res = requests.post("https://hate-speech-topic-c2eedpqzcq-an.a.run.app/predict", json = {"input_sentences": input_sentences}) 48 | if res.status_code != 200: 49 | raise Exception("Error: API request unsuccessful.") 50 | data = res.json() 51 | return data['predictions'] 52 | 53 | 54 | def analyze(input_sentences): 55 | binary_pred = analyze_binary(input_sentences) 56 | topic_input_sentences = [pred['sentence'] for pred in binary_pred if pred['label']=='혐오'] 57 | data = analyze_topic(topic_input_sentences) 58 | return data 59 | 60 | 61 | @app.route("/") 62 | def index(): 63 | requests.post("https://hate-speech-binary-c2eedpqzcq-an.a.run.app/wake", json = {"wake": 'wake'}) 64 | requests.post("https://hate-speech-topic-c2eedpqzcq-an.a.run.app/wake", json = {"wake": 'wake'}) 65 | return render_template("index.html") 66 | 67 | 68 | @app.route("/scrape_page") 69 | def scrape_page(): 70 | return render_template("scrape_page.html") 71 | 72 | 73 | @app.route("/input_page") 74 | def input_page(): 75 | return render_template("input_page.html") 76 | 77 | 78 | @app.route("/loading") 79 | def loading(): 80 | input_sentences = request.args.to_dict()['input_sentences'] 81 | input_sentences = input_sentences.strip('][').replace('\' ','').replace('\'','').split(', ') 82 | print(str(input_sentences)) 83 | sys.stdout.flush() 84 | sentence_pairs = analyze(input_sentences) 85 | if len(sentence_pairs)==0: 86 | activation=False 87 | else: 88 | activation=True 89 | return render_template("results.html", sentence_pairs=sentence_pairs, activation=activation) 90 | 91 | 92 | @app.route("/scrape", methods=["POST"]) 93 | def scrape(): 94 | url = request.form.get("url") 95 | article_text = scrape_url(url) 96 | input_sentences = text_preprocessing(article_text) 97 | input_sentences = {'input_sentences': input_sentences} 98 | input_sentences = urlencode(input_sentences) 99 | return render_template("loading.html", input_sentences=input_sentences) 100 | 101 | 102 | @app.route("/input", methods=["POST"]) 103 | def input(): 104 | input_text = request.form.get("input_text") 105 | input_sentences = text_preprocessing(input_text) 106 | input_sentences = {'input_sentences': input_sentences} 107 | input_sentences = urlencode(input_sentences) 108 | return render_template("loading.html", input_sentences=input_sentences) 109 | 110 | 111 | if __name__ == "__main__": 112 | app.run(debug=True, host='0.0.0.0', port=int(os.environ.get('PORT', 8080))) 113 | -------------------------------------------------------------------------------- /app/hate_speech_main/environment.yml: -------------------------------------------------------------------------------- 1 | name: hate_env 2 | dependencies: 3 | - nomkl 4 | - python=3.7 5 | - pip 6 | - pip: 7 | - Flask==1.1.2 8 | - gunicorn==20.0.4 9 | - Jinja2==2.11.1 10 | - requests==2.23.0 11 | - urllib3==1.25.8 12 | - Werkzeug==1.0.0 13 | - newspaper3k==0.2.8 14 | -------------------------------------------------------------------------------- /app/hate_speech_main/static/css/index.css: -------------------------------------------------------------------------------- 1 | a { 2 | color: inherit; 3 | text-decoration: inherit; 4 | } 5 | 6 | @font-face { 7 | font-family: trial; 8 | src: {{ url_for('static', filename='fonts/NanumSquareB.ttf') }}; 9 | } 10 | 11 | body { 12 | margin: 0; 13 | padding: 0; 14 | } 15 | ::selection { 16 | background-color: #3D3D3D; 17 | color: #1b1b1b; 18 | } 19 | nav { 20 | width: 100%; 21 | background-color: #0b0b0b; 22 | position: fixed; 23 | bottom: 0; 24 | height: 100px; 25 | overflow: hidden; 26 | } 27 | nav ul { 28 | position: absolute; 29 | top: 0; 30 | left: 0; 31 | list-style-type: none; 32 | padding: 0; 33 | margin: 0; 34 | width: 140vw; 35 | } 36 | nav li { 37 | margin: 0; 38 | float: left; 39 | display: inline-block; 40 | height: 98px; 41 | margin-bottom: 2px; 42 | line-height: 100px; 43 | width: 20vw; 44 | text-align: center; 45 | color: #555; 46 | transition: background-color 0.5s 0.2s ease, color 0.3s ease; 47 | cursor: pointer; 48 | text-transform: uppercase; 49 | letter-spacing: 4px; 50 | transform: translateY(100%); 51 | } 52 | nav li.active { 53 | background-color: #151515; 54 | color: #efefef; 55 | } 56 | nav li.active::after { 57 | content: ""; 58 | position: absolute; 59 | bottom: -2px; 60 | left: 0; 61 | width: 100%; 62 | height: 2px; 63 | background-color: #f7ca18; 64 | } 65 | nav li:nth-child(1) { 66 | animation: pop 0.5s 0.15s 1 forwards; 67 | } 68 | nav li:nth-child(2) { 69 | animation: pop 0.5s 0.3s 1 forwards; 70 | } 71 | nav li:nth-child(3) { 72 | animation: pop 0.5s 0.45s 1 forwards; 73 | } 74 | nav li:nth-child(4) { 75 | animation: pop 0.5s 0.6s 1 forwards; 76 | } 77 | nav li:nth-child(5) { 78 | animation: pop 0.5s 0.75s 1 forwards; 79 | } 80 | nav li:nth-child(6) { 81 | animation: pop 0.5s 0.9s 1 forwards; 82 | } 83 | nav li:nth-child(7) { 84 | animation: pop 0.5s 1.05s 1 forwards; 85 | } 86 | nav li:hover { 87 | color: #ececec; 88 | } 89 | nav li:active { 90 | background-color: #222; 91 | } 92 | nav li:nth-child(1):hover ~ .slide { 93 | left: 0; 94 | } 95 | nav li:nth-child(2):hover ~ .slide { 96 | left: 20vw; 97 | } 98 | nav li:nth-child(3):hover ~ .slide { 99 | left: 40vw; 100 | } 101 | nav li:nth-child(4):hover ~ .slide { 102 | left: 60vw; 103 | } 104 | nav li:nth-child(5):hover ~ .slide { 105 | left: 80vw; 106 | } 107 | nav li:nth-child(6):hover ~ .slide { 108 | left: 100vw; 109 | } 110 | nav li:nth-child(7):hover ~ .slide { 111 | left: 120vw; 112 | } 113 | nav li.slide { 114 | position: absolute; 115 | left: -20vw; 116 | top: 0; 117 | background-color: #fff; 118 | z-index: -1; 119 | height: 2px; 120 | margin-top: 98px; 121 | transition: left 0.3s ease; 122 | transform: translateY(0); 123 | } 124 | section { 125 | background-color: #3D3D3D; 126 | height: 100vh; 127 | display: flex; 128 | } 129 | section .title { 130 | max-width: 60%; 131 | width: 100%; 132 | align-self: center; 133 | transform: translateY(-50px); 134 | margin: 0 auto; 135 | overflow: hidden; 136 | padding-bottom: 10px; 137 | } 138 | section .title span { 139 | display: inline-block; 140 | color: #efefef; 141 | width: 100%; 142 | text-transform: uppercase; 143 | transform: translateX(-100%); 144 | animation: byBottom 1s ease both; 145 | letter-spacing: 0.25vw; 146 | } 147 | section .title span:last-child { 148 | animation: byBottom 1s 0.25s ease both; 149 | } 150 | section .title span a { 151 | position: relative; 152 | display: inline-block; 153 | margin-left: 0.5rem; 154 | text-decoration: none; 155 | color: #f7ca18; 156 | } 157 | section .title span a::after { 158 | content: ""; 159 | height: 2px; 160 | background-color: #f7ca18; 161 | position: absolute; 162 | bottom: -10px; 163 | left: 0; 164 | width: 0; 165 | animation: linkAfter 0.5s 1s ease both; 166 | } 167 | @-moz-keyframes pop { 168 | 0% { 169 | transform: translateY(100%); 170 | } 171 | 100% { 172 | transform: translateY(0); 173 | } 174 | } 175 | @-webkit-keyframes pop { 176 | 0% { 177 | transform: translateY(100%); 178 | } 179 | 100% { 180 | transform: translateY(0); 181 | } 182 | } 183 | @-o-keyframes pop { 184 | 0% { 185 | transform: translateY(100%); 186 | } 187 | 100% { 188 | transform: translateY(0); 189 | } 190 | } 191 | @keyframes pop { 192 | 0% { 193 | transform: translateY(100%); 194 | } 195 | 100% { 196 | transform: translateY(0); 197 | } 198 | } 199 | @-moz-keyframes byBottom { 200 | 0% { 201 | transform: translateY(150%); 202 | } 203 | 100% { 204 | transform: translateY(0); 205 | } 206 | } 207 | @-webkit-keyframes byBottom { 208 | 0% { 209 | transform: translateY(150%); 210 | } 211 | 100% { 212 | transform: translateY(0); 213 | } 214 | } 215 | @-o-keyframes byBottom { 216 | 0% { 217 | transform: translateY(150%); 218 | } 219 | 100% { 220 | transform: translateY(0); 221 | } 222 | } 223 | @keyframes byBottom { 224 | 0% { 225 | transform: translateY(150%); 226 | } 227 | 100% { 228 | transform: translateY(0); 229 | } 230 | } 231 | @-moz-keyframes linkAfter { 232 | 0% { 233 | width: 0; 234 | } 235 | 100% { 236 | width: 30px; 237 | } 238 | } 239 | @-webkit-keyframes linkAfter { 240 | 0% { 241 | width: 0; 242 | } 243 | 100% { 244 | width: 30px; 245 | } 246 | } 247 | @-o-keyframes linkAfter { 248 | 0% { 249 | width: 0; 250 | } 251 | 100% { 252 | width: 30px; 253 | } 254 | } 255 | @keyframes linkAfter { 256 | 0% { 257 | width: 0; 258 | } 259 | 100% { 260 | width: 30px; 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /app/hate_speech_main/static/css/loading.css: -------------------------------------------------------------------------------- 1 | .loader { 2 | position: relative; 3 | width: 2.5em; 4 | height: 2.5em; 5 | transform: rotate(165deg); 6 | } 7 | .loader:before, .loader:after { 8 | content: ''; 9 | position: absolute; 10 | top: 50%; 11 | left: 50%; 12 | display: block; 13 | width: 0.5em; 14 | height: 0.5em; 15 | border-radius: 0.25em; 16 | transform: translate(-50%, -50%); 17 | } 18 | .loader:before { 19 | animation: before 2s infinite; 20 | } 21 | .loader:after { 22 | animation: after 2s infinite; 23 | } 24 | 25 | @keyframes before { 26 | 0% { 27 | width: 0.5em; 28 | box-shadow: 1em -0.5em rgba(225, 20, 98, 0.75), -1em 0.5em rgba(225, 20, 98, 0.75); 29 | } 30 | 35% { 31 | width: 2.5em; 32 | box-shadow: 0 -0.5em rgba(225, 20, 98, 0.75), 0 0.5em rgba(225, 20, 98, 0.75); 33 | } 34 | 70% { 35 | width: 0.5em; 36 | box-shadow: -1em -0.5em rgba(225, 20, 98, 0.75), 1em 0.5em rgba(225, 20, 98, 0.75); 37 | } 38 | 100% { 39 | box-shadow: 1em -0.5em rgba(225, 20, 98, 0.75), -1em 0.5em rgba(225, 20, 98, 0.75); 40 | } 41 | } 42 | @keyframes after { 43 | 0% { 44 | height: 0.5em; 45 | box-shadow: 0.5em 1em rgba(225, 20, 98, 0.75), -0.5em -1em rgba(225, 20, 98, 0.75); 46 | } 47 | 35% { 48 | height: 2.5em; 49 | box-shadow: 0.5em 0 rgba(225, 20, 98, 0.75), -0.5em 0 rgba(225, 20, 98, 0.75); 50 | } 51 | 70% { 52 | height: 0.5em; 53 | box-shadow: 0.5em -1em rgba(225, 20, 98, 0.75), -0.5em 1em rgba(225, 20, 98, 0.75); 54 | } 55 | 100% { 56 | box-shadow: 0.5em 1em rgba(225, 20, 98, 0.75), -0.5em -1em rgba(225, 20, 98, 0.75); 57 | } 58 | } 59 | /** 60 | * Attempt to center the whole thing! 61 | */ 62 | html, 63 | body { 64 | height: 100%; 65 | } 66 | 67 | .loader { 68 | position: absolute; 69 | top: calc(50% - 1.25em); 70 | left: calc(50% - 1.25em); 71 | } 72 | -------------------------------------------------------------------------------- /app/hate_speech_main/static/css/page.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | * { 5 | margin: 0px; 6 | padding: 0px; 7 | box-sizing: border-box; 8 | } 9 | 10 | body, html { 11 | height: 100%; 12 | font-family: Ubuntu-Bold, sans-serif; 13 | } 14 | 15 | /*---------------------------------------------*/ 16 | a { 17 | font-family: Ubuntu-Bold; 18 | font-size: 14px; 19 | line-height: 1.7; 20 | color: #666666; 21 | margin: 0px; 22 | transition: all 0.4s; 23 | -webkit-transition: all 0.4s; 24 | -o-transition: all 0.4s; 25 | -moz-transition: all 0.4s; 26 | } 27 | 28 | a:focus { 29 | outline: none !important; 30 | } 31 | 32 | a:hover { 33 | text-decoration: none; 34 | } 35 | 36 | /*---------------------------------------------*/ 37 | h1,h2,h3,h4,h5,h6 { 38 | margin: 0px; 39 | } 40 | 41 | p { 42 | font-family: Ubuntu-Bold; 43 | font-size: 14px; 44 | line-height: 1.7; 45 | color: #666666; 46 | margin: 0px; 47 | } 48 | 49 | ul, li { 50 | margin: 0px; 51 | list-style-type: none; 52 | } 53 | 54 | 55 | /*---------------------------------------------*/ 56 | input { 57 | outline: none; 58 | border: none; 59 | } 60 | 61 | input[type="number"] { 62 | -moz-appearance: textfield; 63 | appearance: none; 64 | -webkit-appearance: none; 65 | } 66 | 67 | input[type="number"]::-webkit-outer-spin-button, 68 | input[type="number"]::-webkit-inner-spin-button { 69 | -webkit-appearance: none; 70 | } 71 | 72 | textarea { 73 | outline: none; 74 | border: none; 75 | } 76 | 77 | textarea:focus, input:focus { 78 | border-color: transparent !important; 79 | } 80 | 81 | input::-webkit-input-placeholder { color: #bdbdd3;} 82 | input:-moz-placeholder { color: #bdbdd3;} 83 | input::-moz-placeholder { color: #bdbdd3;} 84 | input:-ms-input-placeholder { color: #bdbdd3;} 85 | 86 | textarea::-webkit-input-placeholder { color: #bdbdd3;} 87 | textarea:-moz-placeholder { color: #bdbdd3;} 88 | textarea::-moz-placeholder { color: #bdbdd3;} 89 | textarea:-ms-input-placeholder { color: #bdbdd3;} 90 | 91 | /*---------------------------------------------*/ 92 | button { 93 | outline: none !important; 94 | border: none; 95 | background: transparent; 96 | } 97 | 98 | button:hover { 99 | cursor: pointer; 100 | } 101 | 102 | iframe { 103 | border: none !important; 104 | } 105 | 106 | 107 | /*---------------------------------------------*/ 108 | .container { 109 | max-width: 1200px; 110 | } 111 | 112 | 113 | 114 | 115 | /*////////////////////////////////////////////////////////////////// 116 | [ Contact ]*/ 117 | 118 | .container-contact100 { 119 | width: 100%; 120 | min-height: 100vh; 121 | display: -webkit-box; 122 | display: -webkit-flex; 123 | display: -moz-box; 124 | display: -ms-flexbox; 125 | display: flex; 126 | flex-wrap: wrap; 127 | justify-content: center; 128 | align-items: center; 129 | padding: 15px; 130 | position: relative; 131 | background-color: #3D3D3D; 132 | } 133 | 134 | .wrap-contact100 { 135 | width: 550px; 136 | background: transparent; 137 | padding: 50px 0px 160px 0px; 138 | } 139 | 140 | 141 | /*================================================================== 142 | [ Form ]*/ 143 | 144 | .contact100-form { 145 | width: 100%; 146 | } 147 | 148 | .contact100-form-title { 149 | display: block; 150 | font-family: Ubuntu-Bold; 151 | font-size: 19px; 152 | color: #FFFFFF; 153 | line-height: 1.2; 154 | text-transform: uppercase; 155 | text-align: center; 156 | padding-bottom: 49px; 157 | } 158 | 159 | /*------------------------------------------------------------------ 160 | [ Input ]*/ 161 | 162 | .wrap-input100 { 163 | width: 100%; 164 | background-color: #fff; 165 | border-radius: 31px; 166 | margin-bottom: 16px; 167 | position: relative; 168 | z-index: 1; 169 | } 170 | 171 | .input100 { 172 | position: relative; 173 | display: block; 174 | width: 100%; 175 | background: #fff; 176 | border-radius: 31px; 177 | font-family: Ubuntu-Bold; 178 | font-size: 16px; 179 | color: #000000; 180 | line-height: 1.2; 181 | } 182 | 183 | 184 | /*---------------------------------------------*/ 185 | input.input100 { 186 | height: 62px; 187 | padding: 0 35px 0 35px; 188 | } 189 | 190 | 191 | textarea.input100 { 192 | min-height: 169px; 193 | padding: 19px 35px 0 35px; 194 | } 195 | 196 | /*------------------------------------------------------------------ 197 | [ Focus Input ]*/ 198 | 199 | .focus-input100 { 200 | display: block; 201 | position: absolute; 202 | z-index: -1; 203 | width: 100%; 204 | height: 100%; 205 | top: 0; 206 | left: 50%; 207 | -webkit-transform: translateX(-50%); 208 | -moz-transform: translateX(-50%); 209 | -ms-transform: translateX(-50%); 210 | -o-transform: translateX(-50%); 211 | transform: translateX(-50%); 212 | border-radius: 31px; 213 | background-color: #fff; 214 | pointer-events: none; 215 | 216 | -webkit-transition: all 0.4s; 217 | -o-transition: all 0.4s; 218 | -moz-transition: all 0.4s; 219 | transition: all 0.4s; 220 | } 221 | 222 | .input100:focus + .focus-input100 { 223 | width: calc(100% + 20px); 224 | } 225 | 226 | /*------------------------------------------------------------------ 227 | [ Button ]*/ 228 | .container-contact100-form-btn { 229 | display: -webkit-box; 230 | display: -webkit-flex; 231 | display: -moz-box; 232 | display: -ms-flexbox; 233 | display: flex; 234 | flex-wrap: wrap; 235 | justify-content: center; 236 | padding-top: 10px; 237 | } 238 | 239 | .contact100-form-btn { 240 | display: -webkit-box; 241 | display: -webkit-flex; 242 | display: -moz-box; 243 | display: -ms-flexbox; 244 | display: flex; 245 | justify-content: center; 246 | align-items: center; 247 | padding: 0 20px; 248 | min-width: 150px; 249 | height: 62px; 250 | background-color: transparent; 251 | border-radius: 31px; 252 | 253 | font-family: Ubuntu-Bold; 254 | font-size: 16px; 255 | color: #fff; 256 | line-height: 1.2; 257 | text-transform: uppercase; 258 | 259 | -webkit-transition: all 0.4s; 260 | -o-transition: all 0.4s; 261 | -moz-transition: all 0.4s; 262 | transition: all 0.4s; 263 | position: relative; 264 | z-index: 1; 265 | } 266 | 267 | .contact100-form-btn::before { 268 | content: ""; 269 | display: block; 270 | position: absolute; 271 | z-index: -1; 272 | width: 100%; 273 | height: 100%; 274 | top: 0; 275 | left: 50%; 276 | -webkit-transform: translateX(-50%); 277 | -moz-transform: translateX(-50%); 278 | -ms-transform: translateX(-50%); 279 | -o-transform: translateX(-50%); 280 | transform: translateX(-50%); 281 | border-radius: 31px; 282 | background-color: #E11462; 283 | pointer-events: none; 284 | 285 | -webkit-transition: all 0.4s; 286 | -o-transition: all 0.4s; 287 | -moz-transition: all 0.4s; 288 | transition: all 0.4s; 289 | } 290 | 291 | .contact100-form-btn:hover:before { 292 | background-color: #E11462; 293 | width: calc(100% + 20px); 294 | } 295 | 296 | 297 | 298 | 299 | /*------------------------------------------------------------------ 300 | [ Alert validate ]*/ 301 | 302 | .validate-input { 303 | position: relative; 304 | } 305 | 306 | .alert-validate::before { 307 | content: attr(data-validate); 308 | position: absolute; 309 | z-index: 1000; 310 | max-width: 70%; 311 | background-color: #fff; 312 | border: 1px solid #c80000; 313 | border-radius: 14px; 314 | padding: 4px 25px 4px 10px; 315 | top: 50%; 316 | -webkit-transform: translateY(-50%); 317 | -moz-transform: translateY(-50%); 318 | -ms-transform: translateY(-50%); 319 | -o-transform: translateY(-50%); 320 | transform: translateY(-50%); 321 | right: 10px; 322 | pointer-events: none; 323 | 324 | font-family: Ubuntu-Bold; 325 | color: #c80000; 326 | font-size: 13px; 327 | line-height: 1.4; 328 | text-align: left; 329 | 330 | visibility: hidden; 331 | opacity: 0; 332 | 333 | -webkit-transition: opacity 0.4s; 334 | -o-transition: opacity 0.4s; 335 | -moz-transition: opacity 0.4s; 336 | transition: opacity 0.4s; 337 | } 338 | 339 | .alert-validate::after { 340 | content: "\f06a"; 341 | font-family: FontAwesome; 342 | display: block; 343 | position: absolute; 344 | z-index: 1100; 345 | color: #c80000; 346 | font-size: 16px; 347 | top: 50%; 348 | -webkit-transform: translateY(-50%); 349 | -moz-transform: translateY(-50%); 350 | -ms-transform: translateY(-50%); 351 | -o-transform: translateY(-50%); 352 | transform: translateY(-50%); 353 | right: 16px; 354 | } 355 | 356 | .alert-validate:hover:before { 357 | visibility: visible; 358 | opacity: 1; 359 | } 360 | 361 | @media (max-width: 992px) { 362 | .alert-validate::before { 363 | visibility: visible; 364 | opacity: 1; 365 | } 366 | } 367 | -------------------------------------------------------------------------------- /app/hate_speech_main/static/css/results.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #3D3D3D; 3 | } 4 | body * { 5 | box-sizing: border-box; 6 | } 7 | 8 | .header { 9 | background-color: #E11462; 10 | color: white; 11 | font-size: 1.5em; 12 | padding: 1rem; 13 | text-align: center; 14 | text-transform: uppercase; 15 | } 16 | 17 | img { 18 | border-radius: 50%; 19 | height: 60px; 20 | width: 60px; 21 | } 22 | 23 | .table-users { 24 | border: 1px solid #E11462; 25 | border-radius: 10px; 26 | box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.1); 27 | max-width: calc(100% - 2em); 28 | margin: 1em auto; 29 | overflow: hidden; 30 | width: 800px; 31 | } 32 | 33 | table { 34 | width: 100%; 35 | } 36 | table td, table th { 37 | color: #E11462; 38 | padding: 10px; 39 | } 40 | table td { 41 | text-align: center; 42 | vertical-align: middle; 43 | } 44 | table td:last-child { 45 | font-size: 0.95em; 46 | line-height: 1.4; 47 | text-align: left; 48 | } 49 | table th { 50 | background-color: #FFB4D1; 51 | font-weight: 300; 52 | } 53 | table tr:nth-child(2n) { 54 | background-color: white; 55 | } 56 | table tr:nth-child(2n+1) { 57 | background-color: #FFB4D1; 58 | } 59 | 60 | @media screen and (max-width: 700px) { 61 | table, tr, td { 62 | display: block; 63 | } 64 | 65 | td:first-child { 66 | position: absolute; 67 | top: 50%; 68 | -webkit-transform: translateY(-50%); 69 | transform: translateY(-50%); 70 | width: 100px; 71 | } 72 | td:not(:first-child) { 73 | clear: both; 74 | margin-left: 100px; 75 | padding: 4px 20px 4px 90px; 76 | position: relative; 77 | text-align: left; 78 | } 79 | td:not(:first-child):before { 80 | color: #91ced4; 81 | content: ''; 82 | display: block; 83 | left: 0; 84 | position: absolute; 85 | } 86 | td:nth-child(2):before { 87 | content: 'Name:'; 88 | } 89 | td:nth-child(3):before { 90 | content: 'Email:'; 91 | } 92 | td:nth-child(4):before { 93 | content: 'Phone:'; 94 | } 95 | td:nth-child(5):before { 96 | content: 'Comments:'; 97 | } 98 | 99 | tr { 100 | padding: 10px 0; 101 | position: relative; 102 | } 103 | tr:first-child { 104 | display: none; 105 | } 106 | } 107 | @media screen and (max-width: 500px) { 108 | .header { 109 | background-color: transparent; 110 | color: white; 111 | font-size: 2em; 112 | font-weight: 700; 113 | padding: 0; 114 | text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.1); 115 | } 116 | 117 | img { 118 | border: 3px solid; 119 | border-color: #daeff1; 120 | height: 100px; 121 | margin: 0.5rem 0; 122 | width: 100px; 123 | } 124 | 125 | td:first-child { 126 | background-color: #c8e7ea; 127 | border-bottom: 1px solid #91ced4; 128 | border-radius: 10px 10px 0 0; 129 | position: relative; 130 | top: 0; 131 | -webkit-transform: translateY(0); 132 | transform: translateY(0); 133 | width: 100%; 134 | } 135 | td:not(:first-child) { 136 | margin: 0; 137 | padding: 5px 1em; 138 | width: 100%; 139 | } 140 | td:not(:first-child):before { 141 | font-size: .8em; 142 | padding-top: 0.3em; 143 | position: relative; 144 | } 145 | td:last-child { 146 | padding-bottom: 1rem !important; 147 | } 148 | 149 | tr { 150 | background-color: white !important; 151 | border: 1px solid #6cbec6; 152 | border-radius: 10px; 153 | box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.1); 154 | margin: 0.5rem 0; 155 | padding: 0; 156 | } 157 | 158 | .table-users { 159 | border: none; 160 | box-shadow: none; 161 | overflow: visible; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /app/hate_speech_main/static/fonts/NanumSquareB.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/captainnemo9292/hate-speech-language-modeling/c5acd039e9a05837a749faba309ddf889ea61f17/app/hate_speech_main/static/fonts/NanumSquareB.ttf -------------------------------------------------------------------------------- /app/hate_speech_main/static/images/foreign.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/captainnemo9292/hate-speech-language-modeling/c5acd039e9a05837a749faba309ddf889ea61f17/app/hate_speech_main/static/images/foreign.png -------------------------------------------------------------------------------- /app/hate_speech_main/static/images/gender.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/captainnemo9292/hate-speech-language-modeling/c5acd039e9a05837a749faba309ddf889ea61f17/app/hate_speech_main/static/images/gender.png -------------------------------------------------------------------------------- /app/hate_speech_main/static/images/political.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/captainnemo9292/hate-speech-language-modeling/c5acd039e9a05837a749faba309ddf889ea61f17/app/hate_speech_main/static/images/political.png -------------------------------------------------------------------------------- /app/hate_speech_main/static/images/regional.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/captainnemo9292/hate-speech-language-modeling/c5acd039e9a05837a749faba309ddf889ea61f17/app/hate_speech_main/static/images/regional.png -------------------------------------------------------------------------------- /app/hate_speech_main/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Page Title 5 | 6 | 7 | 8 | 9 | 19 |
20 |

혐오 발언 탐지기

인공지능을 활용하여 온라인 혐오 표현을 감지합니다. 직접 텍스트를 작성하거나 웹페이지 URL을 입력하면 글에 숨어있는 혐오 표현을 분석합니다.

21 | 22 |
23 |
24 | 25 |
26 |
27 | 28 |
29 | 30 | 31 | -------------------------------------------------------------------------------- /app/hate_speech_main/templates/input_page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Page Title 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 |
13 |
14 | 15 | 텍스트를 입력하면 글 속에 숨어있는 혐오 표현을 분석합니다 16 | 17 | 18 |
19 | 20 | 21 |
22 | 23 |
24 | 30 |
31 |
32 |
33 |
34 | 35 | 36 | -------------------------------------------------------------------------------- /app/hate_speech_main/templates/loading.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Page Title 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/hate_speech_main/templates/results.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Page Title 5 | 6 | 7 | 8 |
9 |
분석 결과
10 | 11 | {% if activation %} 12 | 13 | 14 | 15 | 16 | 17 | {% for pair in sentence_pairs %} 18 | 19 | {% if pair['label'] == "특정 지역에 대한 차별적 발언" %} 20 | 21 | {% elif pair['label'] == "정치적 성향이 다른 사람들에 대한 혐오 및 왜곡" %} 22 | 23 | {% elif pair['label'] == "다른 나라에 대한 차별적 발언" %} 24 | 25 | {% elif pair['label'] == "여성 및 성소수자에 대한 혐오 및 왜곡" %} 26 | 27 | {% endif %} 28 | 29 | 30 | 31 | {% endfor %} 32 | {% else %} 33 | 34 | {% endif %} 35 |
아이콘혐오 발언차별 대상
Eucalyp from FlaticonNikita Golubev from FlaticonSmashicons from FlaticonFreepik from Flaticon{{ pair['sentence'] }}{{ pair['label'] }}
정상
36 |
37 | 38 | 39 | -------------------------------------------------------------------------------- /app/hate_speech_main/templates/scrape_page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Page Title 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 |
13 |
14 | 15 | URL을 입력하면 웹페이지에 숨어있는 혐오 표현을 분석합니다 16 | 17 | 18 |
19 | 20 | 21 |
22 | 23 |
24 | 30 |
31 |
32 |
33 |
34 | 35 | 36 | -------------------------------------------------------------------------------- /modeling/dataset/README.md: -------------------------------------------------------------------------------- 1 | # hate-speech-language-modeling 2 | the datasets are available at https://www.kaggle.com/captainnemo9292/korean-hate-speech-dataset 3 | -------------------------------------------------------------------------------- /modeling/model/README.md: -------------------------------------------------------------------------------- 1 | # hate-speech-language-modeling 2 | the models are available at https://www.kaggle.com/captainnemo9292/korean-hate-speech-dataset 3 | -------------------------------------------------------------------------------- /modeling/modeling.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Recurrent Neural Network based Hate Speech Language Model for Korean Hate Speech Detection" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "## 1. Data Collection " 15 | ] 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "metadata": {}, 20 | "source": [ 21 | "### 1.1. Scraping Raw Hate Speech Text Data" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": null, 27 | "metadata": { 28 | "scrolled": true 29 | }, 30 | "outputs": [], 31 | "source": [ 32 | "from selenium import webdriver\n", 33 | "import time\n", 34 | "import pandas as pd\n", 35 | "\n", 36 | "list_size = 10000\n", 37 | "url = 'https://www.ilbe.com/list/ilbe?listSize={}&sub=best&listStyle=list'.format(list_size)\n", 38 | "driver = webdriver.Chrome(executable_path = 'D:\\chromedriver_win32\\chromedriver.exe')\n", 39 | "driver.get(url)\n", 40 | "time.sleep(5)\n", 41 | "\n", 42 | "url_list = []\n", 43 | "post_list = driver.find_elements_by_xpath('//ul[contains(@class, \\'board-body\\')]//li[not(@id) and not(@class)]//span[contains(@class, \\'title\\')]//a[contains(@class, \\'subject\\')]')\n", 44 | "for post_num in range(len(post_list)):\n", 45 | " print(post_num, post_list[post_num].get_attribute('href'))\n", 46 | " url_list.append(post_list[post_num].get_attribute('href'))\n", 47 | " pd.Series(url_list).to_frame(name='url').to_csv('url_list_v2.csv')" 48 | ] 49 | }, 50 | { 51 | "cell_type": "code", 52 | "execution_count": null, 53 | "metadata": { 54 | "scrolled": false 55 | }, 56 | "outputs": [], 57 | "source": [ 58 | "from selenium import webdriver\n", 59 | "import time\n", 60 | "import pandas as pd\n", 61 | "\n", 62 | "hate_speech_list = pd.read_csv('hate_speech_raw_data.csv')['hate_speech'].tolist()\n", 63 | "#hate_speech_list = []\n", 64 | "url_list = pd.read_csv('url_list_v2.csv')['url'].tolist()\n", 65 | "\n", 66 | "i = 1185\n", 67 | "driver = webdriver.Chrome(executable_path = 'D:\\chromedriver_win32\\chromedriver.exe')\n", 68 | "for url_num in range(i, len(url_list)):\n", 69 | " driver.get(url_list[url_num])\n", 70 | " comment_list = driver.find_elements_by_xpath('//span[contains(@class, \\'comment-box\\')]')\n", 71 | " for comment in comment_list:\n", 72 | " hate_speech_list.append(comment.text)\n", 73 | " print(url_num, comment.text) \n", 74 | " pd.Series(hate_speech_list).to_frame(name='hate_speech').drop_duplicates().reset_index(drop=True).to_csv('hate_speech_raw_data.csv', index=False) \n", 75 | " time.sleep(5)" 76 | ] 77 | }, 78 | { 79 | "cell_type": "code", 80 | "execution_count": 4, 81 | "metadata": {}, 82 | "outputs": [ 83 | { 84 | "data": { 85 | "text/html": [ 86 | "
\n", 87 | "\n", 100 | "\n", 101 | " \n", 102 | " \n", 103 | " \n", 104 | " \n", 105 | " \n", 106 | " \n", 107 | " \n", 108 | " \n", 109 | " \n", 110 | " \n", 111 | " \n", 112 | " \n", 113 | " \n", 114 | " \n", 115 | " \n", 116 | " \n", 117 | " \n", 118 | " \n", 119 | " \n", 120 | " \n", 121 | " \n", 122 | " \n", 123 | " \n", 124 | " \n", 125 | " \n", 126 | " \n", 127 | " \n", 128 | " \n", 129 | "
hate_speech
129446전라씨발 전라컹새끼들ㅋㅋㅋ
129447ㅈㄹㄷ
1294485월 영상을 지금 들먹이노 ㅋㅋㅋㅋ ㅇㅂ
129449경찰대 나와서 엘리트출신도 아니도 그냥 경찰딱지달고 나부랭이들은 저런 결정권 주면 ...
129450옜날꺼 ㅁㅈㅎ
\n", 130 | "
" 131 | ], 132 | "text/plain": [ 133 | " hate_speech\n", 134 | "129446 전라씨발 전라컹새끼들ㅋㅋㅋ\n", 135 | "129447 ㅈㄹㄷ\n", 136 | "129448 5월 영상을 지금 들먹이노 ㅋㅋㅋㅋ ㅇㅂ\n", 137 | "129449 경찰대 나와서 엘리트출신도 아니도 그냥 경찰딱지달고 나부랭이들은 저런 결정권 주면 ...\n", 138 | "129450 옜날꺼 ㅁㅈㅎ" 139 | ] 140 | }, 141 | "execution_count": 4, 142 | "metadata": {}, 143 | "output_type": "execute_result" 144 | } 145 | ], 146 | "source": [ 147 | "import pandas as pd \n", 148 | "\n", 149 | "data = pd.read_csv('hate_speech_raw_data.csv')\n", 150 | "data.tail()" 151 | ] 152 | }, 153 | { 154 | "cell_type": "markdown", 155 | "metadata": {}, 156 | "source": [ 157 | "## 2. Data Preprocessing" 158 | ] 159 | }, 160 | { 161 | "cell_type": "markdown", 162 | "metadata": {}, 163 | "source": [ 164 | "### 2.1. Text Preprocessing with KoNLPY" 165 | ] 166 | }, 167 | { 168 | "cell_type": "code", 169 | "execution_count": null, 170 | "metadata": { 171 | "scrolled": true 172 | }, 173 | "outputs": [], 174 | "source": [ 175 | "from konlpy.tag import Okt \n", 176 | "import pandas as pd \n", 177 | "\n", 178 | "okt = Okt()\n", 179 | "data = pd.read_csv('hate_speech_raw_data.csv')\n", 180 | "\n", 181 | "for i in range(len(data['hate_speech'])):\n", 182 | " \n", 183 | " sentence = ''\n", 184 | " try:\n", 185 | " for word in okt.nouns(data['hate_speech'][i]):\n", 186 | " sentence = sentence + ' ' + word\n", 187 | " except:\n", 188 | " pass\n", 189 | " data['hate_speech'][i] = sentence\n", 190 | " print(i ,data['hate_speech'][i])\n", 191 | " data.to_csv('hate_speech_data.csv', index=False)" 192 | ] 193 | }, 194 | { 195 | "cell_type": "markdown", 196 | "metadata": {}, 197 | "source": [ 198 | "### 2.2. Topic Modeling" 199 | ] 200 | }, 201 | { 202 | "cell_type": "code", 203 | "execution_count": 3, 204 | "metadata": {}, 205 | "outputs": [ 206 | { 207 | "data": { 208 | "text/html": [ 209 | "
\n", 210 | "\n", 223 | "\n", 224 | " \n", 225 | " \n", 226 | " \n", 227 | " \n", 228 | " \n", 229 | " \n", 230 | " \n", 231 | " \n", 232 | " \n", 233 | " \n", 234 | " \n", 235 | " \n", 236 | " \n", 237 | " \n", 238 | " \n", 239 | " \n", 240 | " \n", 241 | " \n", 242 | " \n", 243 | " \n", 244 | " \n", 245 | " \n", 246 | " \n", 247 | " \n", 248 | " \n", 249 | " \n", 250 | " \n", 251 | " \n", 252 | "
hate_speech
129446전라 전라 컹
129447
129448영상
129449경찰대 엘리트 출신 경찰 달 나부랭이 저런 결정 줫 뭔가 결정
129450옜날꺼
\n", 253 | "
" 254 | ], 255 | "text/plain": [ 256 | " hate_speech\n", 257 | "129446 전라 전라 컹 \n", 258 | "129447 \n", 259 | "129448 영상 \n", 260 | "129449 경찰대 엘리트 출신 경찰 달 나부랭이 저런 결정 줫 뭔가 결정\n", 261 | "129450 옜날꺼" 262 | ] 263 | }, 264 | "execution_count": 3, 265 | "metadata": {}, 266 | "output_type": "execute_result" 267 | } 268 | ], 269 | "source": [ 270 | "import pandas as pd\n", 271 | "\n", 272 | "text = pd.read_csv('hate_speech_data.csv').fillna(' ').replace(to_replace=['존나', '진짜', '사람', '나라', '생각', '이건', '씨발', '시발', '일베', '익명', '병신', '재앙', '문재인', '게이', '이기', '댓글', '정보', '새끼', '지랄', '개새끼', '그냥', '보고', '아주', '얼굴', '한국', '우리', '지금', '대통령', '홍어', '분탕'], value=\"\",regex=True)\n", 273 | "text.tail()" 274 | ] 275 | }, 276 | { 277 | "cell_type": "code", 278 | "execution_count": 4, 279 | "metadata": {}, 280 | "outputs": [ 281 | { 282 | "data": { 283 | "text/plain": [ 284 | "(129451, 10000)" 285 | ] 286 | }, 287 | "execution_count": 4, 288 | "metadata": {}, 289 | "output_type": "execute_result" 290 | } 291 | ], 292 | "source": [ 293 | "from sklearn.feature_extraction.text import TfidfVectorizer\n", 294 | "vectorizer = TfidfVectorizer(max_features= 10000) # 상위 10,000개의 단어를 보존 \n", 295 | "X = vectorizer.fit_transform(text['hate_speech'])\n", 296 | "X.shape " 297 | ] 298 | }, 299 | { 300 | "cell_type": "code", 301 | "execution_count": 5, 302 | "metadata": {}, 303 | "outputs": [], 304 | "source": [ 305 | "from sklearn.decomposition import NMF\n", 306 | "\n", 307 | "nmf_model = NMF(n_components=5,random_state=777,max_iter=1).fit(X)\n", 308 | "nmf_top = nmf_model.fit_transform(X)\n", 309 | "terms = vectorizer.get_feature_names()" 310 | ] 311 | }, 312 | { 313 | "cell_type": "code", 314 | "execution_count": 18, 315 | "metadata": { 316 | "scrolled": false 317 | }, 318 | "outputs": [ 319 | { 320 | "name": "stdout", 321 | "output_type": "stream", 322 | "text": [ 323 | "Topic 1: [('전라도', 4.96), ('출신', 0.09), ('경상도', 0.08), ('민주당', 0.07), ('고향', 0.07), ('때문', 0.07), ('대한민국', 0.06), ('서울', 0.05), ('지역', 0.05), ('광주', 0.05), ('세상', 0.04), ('지지율', 0.04), ('인간', 0.04), ('팩트', 0.04), ('특징', 0.04), ('사투리', 0.03), ('좌빨', 0.03), ('문제', 0.03), ('어디', 0.03), ('하나', 0.03), ('부모', 0.03), ('사회', 0.03), ('김대중', 0.03), ('부산', 0.03), ('혐오', 0.03), ('저런', 0.03), ('지지', 0.03), ('역시', 0.03), ('쓰레기', 0.03), ('북한', 0.03), ('독립', 0.03), ('사절', 0.03), ('정말', 0.03), ('이제', 0.03), ('동네', 0.02), ('통수', 0.02), ('거지', 0.02), ('버러지', 0.02), ('고향이', 0.02), ('카르텔', 0.02), ('폭동', 0.02), ('대구', 0.02), ('과학', 0.02), ('자기', 0.02), ('기업', 0.02), ('유전자', 0.02), ('자체', 0.02), ('조선족', 0.02), ('정도', 0.02), ('원래', 0.02)]\n", 324 | "Topic 2: [('좌파', 3.04), ('우파', 0.99), ('소리', 0.97), ('국민', 0.83), ('미국', 0.79), ('개돼지', 0.77), ('북한', 0.77), ('정권', 0.7), ('박근혜', 0.68), ('하나', 0.64), ('수준', 0.6), ('좌빨', 0.57), ('보수', 0.57), ('선동', 0.56), ('정도', 0.56), ('때문', 0.55), ('문제', 0.55), ('대가리', 0.53), ('저런', 0.51), ('정치', 0.5), ('좌좀', 0.5), ('탄핵', 0.48), ('일본', 0.45), ('대한민국', 0.44), ('노무현', 0.44), ('이제', 0.43), ('자기', 0.39), ('무슨', 0.38), ('정부', 0.37), ('국가', 0.36), ('사실', 0.35), ('짱깨', 0.35), ('역시', 0.34), ('언론', 0.34), ('팩트', 0.33), ('쓰레기', 0.32), ('민주당', 0.32), ('이유', 0.31), ('인간', 0.31), ('누가', 0.29), ('거지', 0.29), ('어디', 0.27), ('정신', 0.26), ('보지', 0.25), ('방송', 0.25), ('자체', 0.24), ('애국', 0.24), ('자유', 0.23), ('조선', 0.23), ('정말', 0.23)]\n", 325 | "Topic 3: [('중국', 3.26), ('북한', 0.79), ('미국', 0.68), ('짱깨', 0.24), ('일본', 0.19), ('간첩', 0.19), ('짱개', 0.1), ('공산당', 0.06), ('대만', 0.06), ('조선족', 0.05), ('러시아', 0.05), ('중국인', 0.04), ('시진핑', 0.04), ('입국', 0.04), ('전쟁', 0.04), ('폐렴', 0.04), ('속국', 0.04), ('매국노', 0.03), ('우한', 0.03), ('세계', 0.03), ('먼저', 0.03), ('금지', 0.03), ('마스크', 0.03), ('여행', 0.03), ('홍콩', 0.03), ('베트남', 0.03), ('전세계', 0.03), ('자본', 0.03), ('한반도', 0.03), ('붕괴', 0.03), ('남한', 0.02), ('미세먼지', 0.02), ('공장', 0.02), ('화교', 0.02), ('기술', 0.02), ('수출', 0.02), ('사드', 0.02), ('경제', 0.02), ('주적', 0.02), ('바퀴벌레', 0.02), ('트럼프', 0.02), ('식민지', 0.02), ('식인종', 0.02), ('인도', 0.02), ('투자', 0.02), ('국내', 0.02), ('침략', 0.02), ('달러', 0.02), ('필리핀', 0.02), ('스파이', 0.02)]\n", 326 | "Topic 4: [('여자', 3.78), ('남자', 1.03), ('보지', 0.3), ('결혼', 0.26), ('저런', 0.12), ('정도', 0.11), ('김치', 0.09), ('일본', 0.08), ('섹스', 0.08), ('어디', 0.08), ('역시', 0.08), ('페미', 0.07), ('누구', 0.06), ('자기', 0.06), ('인생', 0.06), ('사진', 0.05), ('문제', 0.05), ('한번', 0.05), ('정말', 0.05), ('이해', 0.04), ('외모', 0.04), ('여자도', 0.04), ('남편', 0.04), ('군대', 0.04), ('이유', 0.04), ('가슴', 0.03), ('엄마', 0.03), ('때문', 0.03), ('보통', 0.03), ('여성', 0.03), ('스시', 0.03), ('라면', 0.03), ('먼저', 0.03), ('사랑', 0.03), ('능력', 0.03), ('이상', 0.03), ('강간', 0.03), ('제일', 0.03), ('인기', 0.03), ('투표', 0.03), ('부모', 0.03), ('사실', 0.03), ('이혼', 0.03), ('바람', 0.03), ('무슨', 0.03), ('대부분', 0.03), ('주변', 0.03), ('직업', 0.03), ('그게', 0.03), ('얘기', 0.03)]\n", 327 | "Topic 5: [('빨갱이', 3.9), ('개돼지', 0.19), ('보지', 0.17), ('종북', 0.06), ('간첩', 0.06), ('토착', 0.05), ('공산당', 0.03), ('주사파', 0.03), ('매국노', 0.03), ('대한민국', 0.03), ('진성', 0.03), ('선동', 0.03), ('문죄인', 0.02), ('개좆', 0.02), ('친일파', 0.02), ('진보', 0.02), ('자유', 0.02), ('이제', 0.02), ('다음', 0.02), ('단체', 0.02), ('애비', 0.02), ('하나', 0.02), ('색기', 0.02), ('좌좀', 0.02), ('임종석', 0.02), ('장악', 0.02), ('청와대', 0.02), ('정권', 0.02), ('친일', 0.02), ('문씨', 0.02), ('공산주의', 0.02), ('소굴', 0.02), ('면상', 0.02), ('비서실', 0.02), ('종특', 0.02), ('국가', 0.02), ('척결', 0.02), ('중도', 0.02), ('마리', 0.02), ('북괴', 0.02), ('전향', 0.02), ('자생', 0.02), ('경남', 0.02), ('짓거리', 0.02), ('멸종', 0.02), ('세상', 0.01), ('점점', 0.01), ('로남불', 0.01), ('하여튼', 0.01), ('악마', 0.01)]\n" 328 | ] 329 | } 330 | ], 331 | "source": [ 332 | "def get_topics(components, feature_names, n=50):\n", 333 | " for idx, topic in enumerate(components):\n", 334 | " print(\"Topic %d:\" % (idx+1), [(feature_names[i], topic[i].round(2)) for i in topic.argsort()[:-n - 1:-1]])\n", 335 | "get_topics(nmf_model.components_,terms)" 336 | ] 337 | }, 338 | { 339 | "cell_type": "code", 340 | "execution_count": 3, 341 | "metadata": {}, 342 | "outputs": [ 343 | { 344 | "data": { 345 | "text/html": [ 346 | "
\n", 347 | "\n", 360 | "\n", 361 | " \n", 362 | " \n", 363 | " \n", 364 | " \n", 365 | " \n", 366 | " \n", 367 | " \n", 368 | " \n", 369 | " \n", 370 | " \n", 371 | " \n", 372 | " \n", 373 | " \n", 374 | " \n", 375 | " \n", 376 | " \n", 377 | " \n", 378 | " \n", 379 | " \n", 380 | " \n", 381 | " \n", 382 | " \n", 383 | " \n", 384 | " \n", 385 | " \n", 386 | " \n", 387 | " \n", 388 | " \n", 389 | " \n", 390 | " \n", 391 | " \n", 392 | " \n", 393 | " \n", 394 | " \n", 395 | "
문장혐오 여부
23589신후게이야 ㅠㅠ0
23590최순실 300조 안민돌새끼는 진짜 사기죄로 처넣어야함\\n\\n일본의 무력에 굴복해서 ...0
23591경상도일 확률 1026%3
23592개쌍도가주도하는질서전라도가기생충처럼나라를살1
23593아무래도 우파로 간 김미균을 시기하는\\n좌파들의 공작이 시작 된듯 하다1
\n", 396 | "
" 397 | ], 398 | "text/plain": [ 399 | " 문장 혐오 여부\n", 400 | "23589 신후게이야 ㅠㅠ 0\n", 401 | "23590 최순실 300조 안민돌새끼는 진짜 사기죄로 처넣어야함\\n\\n일본의 무력에 굴복해서 ... 0\n", 402 | "23591 경상도일 확률 1026% 3\n", 403 | "23592 개쌍도가주도하는질서전라도가기생충처럼나라를살 1\n", 404 | "23593 아무래도 우파로 간 김미균을 시기하는\\n좌파들의 공작이 시작 된듯 하다 1" 405 | ] 406 | }, 407 | "execution_count": 3, 408 | "metadata": {}, 409 | "output_type": "execute_result" 410 | } 411 | ], 412 | "source": [ 413 | "import pandas as pd\n", 414 | "\n", 415 | "text_df = pd.read_csv('hate_speech_topic_dataset.csv', index_col=0)\n", 416 | "text_df['문장'] = text_df['문장'].astype('str').replace({'0': \"특정 지역에 대한 차별적 발언\", '1': \"정치적 성향이 다른 사람들에 대한 혐오 및 왜곡\", '2': \"다른 나라에 대한 차별적 발언\", '3': \"여성 및 성소수자에 대한 혐오 및 왜곡\"})\n", 417 | "text_df.tail()" 418 | ] 419 | }, 420 | { 421 | "cell_type": "code", 422 | "execution_count": null, 423 | "metadata": { 424 | "scrolled": true 425 | }, 426 | "outputs": [], 427 | "source": [ 428 | "import pandas as pd\n", 429 | "import numpy as np\n", 430 | "\n", 431 | "text = pd.read_csv('hate_speech_raw_data.csv', index_col=0)\n", 432 | "text['topic'] = np.nan\n", 433 | "\n", 434 | "for i in range(len(nmf_top)):\n", 435 | " text['topic'][i] = np.argmax(nmf_top[i])\n", 436 | " print(i)" 437 | ] 438 | }, 439 | { 440 | "cell_type": "code", 441 | "execution_count": 26, 442 | "metadata": {}, 443 | "outputs": [], 444 | "source": [ 445 | "text[text['topic']!=4.0].reset_index().to_csv('hate_speech_data_cleaned.csv')" 446 | ] 447 | }, 448 | { 449 | "cell_type": "markdown", 450 | "metadata": {}, 451 | "source": [ 452 | "## 3. Language Modeling" 453 | ] 454 | }, 455 | { 456 | "cell_type": "markdown", 457 | "metadata": {}, 458 | "source": [ 459 | "### 3.1. Training Data Preperation" 460 | ] 461 | }, 462 | { 463 | "cell_type": "code", 464 | "execution_count": 27, 465 | "metadata": {}, 466 | "outputs": [ 467 | { 468 | "data": { 469 | "text/plain": [ 470 | "1.0 96009\n", 471 | "0.0 19805\n", 472 | "2.0 5800\n", 473 | "3.0 5795\n", 474 | "Name: topic, dtype: int64" 475 | ] 476 | }, 477 | "execution_count": 27, 478 | "metadata": {}, 479 | "output_type": "execute_result" 480 | } 481 | ], 482 | "source": [ 483 | "import pandas as pd\n", 484 | "\n", 485 | "hate_text = pd.read_csv('hate_speech_data_cleaned.csv')\n", 486 | "hate_text['topic'].value_counts()" 487 | ] 488 | }, 489 | { 490 | "cell_type": "code", 491 | "execution_count": 2, 492 | "metadata": {}, 493 | "outputs": [ 494 | { 495 | "data": { 496 | "text/html": [ 497 | "
\n", 498 | "\n", 511 | "\n", 512 | " \n", 513 | " \n", 514 | " \n", 515 | " \n", 516 | " \n", 517 | " \n", 518 | " \n", 519 | " \n", 520 | " \n", 521 | " \n", 522 | " \n", 523 | " \n", 524 | " \n", 525 | " \n", 526 | " \n", 527 | " \n", 528 | " \n", 529 | " \n", 530 | " \n", 531 | " \n", 532 | " \n", 533 | " \n", 534 | " \n", 535 | " \n", 536 | " \n", 537 | " \n", 538 | " \n", 539 | " \n", 540 | " \n", 541 | " \n", 542 | " \n", 543 | " \n", 544 | " \n", 545 | " \n", 546 | " \n", 547 | " \n", 548 | " \n", 549 | " \n", 550 | " \n", 551 | " \n", 552 | "
iddocumentlabel
999953793074귀신보다 사람이 얼마나 무서운가를 보여주는.. 메시지까지 담고있는 드라마~최고!1
999963025658이라크 및 아랍과의 전쟁을 그린 모든 영화 중에서 가장 최고!!1
999977698359값으로 환산할 수 없을 만큼 귀엽고 황홀한 캐릭터 ㅠㅠ1
9999870686531
999993206900파괴지왕에서 장학우 콘서트 티켓을 얻기위한... ㅋ1
\n", 553 | "
" 554 | ], 555 | "text/plain": [ 556 | " id document label\n", 557 | "99995 3793074 귀신보다 사람이 얼마나 무서운가를 보여주는.. 메시지까지 담고있는 드라마~최고! 1\n", 558 | "99996 3025658 이라크 및 아랍과의 전쟁을 그린 모든 영화 중에서 가장 최고!! 1\n", 559 | "99997 7698359 값으로 환산할 수 없을 만큼 귀엽고 황홀한 캐릭터 ㅠㅠ 1\n", 560 | "99998 7068653 짱 1\n", 561 | "99999 3206900 파괴지왕에서 장학우 콘서트 티켓을 얻기위한... ㅋ 1" 562 | ] 563 | }, 564 | "execution_count": 2, 565 | "metadata": {}, 566 | "output_type": "execute_result" 567 | } 568 | ], 569 | "source": [ 570 | "import pandas as pd\n", 571 | "\n", 572 | "random_text = pd.read_csv('ratings.txt', sep='\\t', quoting=3)\n", 573 | "random_text = random_text[random_text['label'] == 1].reset_index(drop=True)\n", 574 | "random_text.tail()" 575 | ] 576 | }, 577 | { 578 | "cell_type": "code", 579 | "execution_count": null, 580 | "metadata": {}, 581 | "outputs": [], 582 | "source": [ 583 | "import pandas as pd\n", 584 | "\n", 585 | "hate_text = pd.read_csv('hate_speech_data_cleaned.csv')\n", 586 | "random_text = random_text[random_text['label'] == 1].reset_index(drop=True)\n", 587 | "\n", 588 | "dataset_dir = 'hate_speech_binary_dataset.csv'\n", 589 | "dataframe = pd.DataFrame(columns=['문장', '혐오 여부'])\n", 590 | "dataframe.to_csv(dataset_dir, index=False)\n", 591 | "dataframe = pd.read_csv(dataset_dir)\n", 592 | "\n", 593 | "count = 0 \n", 594 | "\n", 595 | "for q in hate_text['hate_speech']:\n", 596 | " data = pd.DataFrame({'문장': q, '혐오 여부': 1}, index=[0])\n", 597 | " dataframe = dataframe.append(data, ignore_index=True)\n", 598 | " dataframe.drop_duplicates().dropna().reset_index(drop=True).to_csv(dataset_dir)\n", 599 | " count = count+1\n", 600 | " print(count)\n", 601 | " \n", 602 | "for t in random_text['document'][:len(hate_text['hate_speech'])]:\n", 603 | " data = pd.DataFrame({'문장': t, '혐오 여부': 0}, index=[0])\n", 604 | " dataframe = dataframe.append(data, ignore_index=True)\n", 605 | " dataframe.drop_duplicates().dropna().reset_index(drop=True).to_csv(dataset_dir)\n", 606 | " count = count+1\n", 607 | " print(count)\n" 608 | ] 609 | }, 610 | { 611 | "cell_type": "code", 612 | "execution_count": 25, 613 | "metadata": {}, 614 | "outputs": [ 615 | { 616 | "data": { 617 | "text/plain": [ 618 | "1 6000\n", 619 | "0 5999\n", 620 | "2 5800\n", 621 | "3 5795\n", 622 | "Name: 혐오 여부, dtype: int64" 623 | ] 624 | }, 625 | "execution_count": 25, 626 | "metadata": {}, 627 | "output_type": "execute_result" 628 | } 629 | ], 630 | "source": [ 631 | "import pandas as pd \n", 632 | "from sklearn.utils import shuffle\n", 633 | "\n", 634 | "dataset_dir = 'hate_speech_topic_dataset.csv'\n", 635 | "df = shuffle(pd.read_csv('hate_speech_topic_dataset.csv', index_col=0)).reset_index(drop=True)\n", 636 | "df.to_csv(dataset_dir)\n", 637 | "df['혐오 여부'].value_counts()" 638 | ] 639 | }, 640 | { 641 | "cell_type": "code", 642 | "execution_count": 11, 643 | "metadata": {}, 644 | "outputs": [ 645 | { 646 | "data": { 647 | "text/html": [ 648 | "
\n", 649 | "\n", 662 | "\n", 663 | " \n", 664 | " \n", 665 | " \n", 666 | " \n", 667 | " \n", 668 | " \n", 669 | " \n", 670 | " \n", 671 | " \n", 672 | " \n", 673 | " \n", 674 | " \n", 675 | " \n", 676 | " \n", 677 | " \n", 678 | " \n", 679 | " \n", 680 | " \n", 681 | " \n", 682 | " \n", 683 | " \n", 684 | " \n", 685 | " \n", 686 | " \n", 687 | " \n", 688 | " \n", 689 | " \n", 690 | " \n", 691 | " \n", 692 | " \n", 693 | " \n", 694 | " \n", 695 | " \n", 696 | " \n", 697 | "
hate_speechtopic
89995그러면 강아지랑 그것도 하셨네1.0
89996목줄안하고있는 개새끼들 죽여도 무죄아님?1.0
89997목줄 안해요 내가 안해요 내가 안하겠다는건데 누가 시킨다는거요1.0
89998왼쪽이 닥터드레냐?1.0
89999@앙마의속삭임 Dr.Dog0.0
\n", 698 | "
" 699 | ], 700 | "text/plain": [ 701 | " hate_speech topic\n", 702 | "89995 그러면 강아지랑 그것도 하셨네 1.0\n", 703 | "89996 목줄안하고있는 개새끼들 죽여도 무죄아님? 1.0\n", 704 | "89997 목줄 안해요 내가 안해요 내가 안하겠다는건데 누가 시킨다는거요 1.0\n", 705 | "89998 왼쪽이 닥터드레냐? 1.0\n", 706 | "89999 @앙마의속삭임 Dr.Dog 0.0" 707 | ] 708 | }, 709 | "execution_count": 11, 710 | "metadata": {}, 711 | "output_type": "execute_result" 712 | } 713 | ], 714 | "source": [ 715 | "import pandas as pd\n", 716 | "\n", 717 | "hate_text = pd.read_csv('hate_speech_data_cleaned.csv', index_col=0)[:90000]\n", 718 | "hate_text.tail()" 719 | ] 720 | }, 721 | { 722 | "cell_type": "code", 723 | "execution_count": 13, 724 | "metadata": {}, 725 | "outputs": [ 726 | { 727 | "data": { 728 | "text/html": [ 729 | "
\n", 730 | "\n", 743 | "\n", 744 | " \n", 745 | " \n", 746 | " \n", 747 | " \n", 748 | " \n", 749 | " \n", 750 | " \n", 751 | " \n", 752 | " \n", 753 | " \n", 754 | " \n", 755 | " \n", 756 | " \n", 757 | " \n", 758 | " \n", 759 | " \n", 760 | " \n", 761 | " \n", 762 | " \n", 763 | " \n", 764 | " \n", 765 | " \n", 766 | " \n", 767 | " \n", 768 | " \n", 769 | " \n", 770 | " \n", 771 | " \n", 772 | " \n", 773 | " \n", 774 | " \n", 775 | " \n", 776 | " \n", 777 | " \n", 778 | "
문장혐오 여부
89995그러면 강아지랑 그것도 하셨네0
89996목줄안하고있는 개새끼들 죽여도 무죄아님?0
89997목줄 안해요 내가 안해요 내가 안하겠다는건데 누가 시킨다는거요0
89998왼쪽이 닥터드레냐?0
89999@앙마의속삭임 Dr.Dog0
\n", 779 | "
" 780 | ], 781 | "text/plain": [ 782 | " 문장 혐오 여부\n", 783 | "89995 그러면 강아지랑 그것도 하셨네 0\n", 784 | "89996 목줄안하고있는 개새끼들 죽여도 무죄아님? 0\n", 785 | "89997 목줄 안해요 내가 안해요 내가 안하겠다는건데 누가 시킨다는거요 0\n", 786 | "89998 왼쪽이 닥터드레냐? 0\n", 787 | "89999 @앙마의속삭임 Dr.Dog 0" 788 | ] 789 | }, 790 | "execution_count": 13, 791 | "metadata": {}, 792 | "output_type": "execute_result" 793 | } 794 | ], 795 | "source": [ 796 | "data = {'문장':hate_text['hate_speech'].tolist(), '혐오 여부':[0] * len(hate_text['hate_speech'].tolist())}\n", 797 | "df_0 = pd.DataFrame(data) \n", 798 | "df_0.tail()" 799 | ] 800 | }, 801 | { 802 | "cell_type": "code", 803 | "execution_count": 14, 804 | "metadata": {}, 805 | "outputs": [ 806 | { 807 | "data": { 808 | "text/html": [ 809 | "
\n", 810 | "\n", 823 | "\n", 824 | " \n", 825 | " \n", 826 | " \n", 827 | " \n", 828 | " \n", 829 | " \n", 830 | " \n", 831 | " \n", 832 | " \n", 833 | " \n", 834 | " \n", 835 | " \n", 836 | " \n", 837 | " \n", 838 | " \n", 839 | " \n", 840 | " \n", 841 | " \n", 842 | " \n", 843 | " \n", 844 | " \n", 845 | " \n", 846 | " \n", 847 | " \n", 848 | " \n", 849 | " \n", 850 | " \n", 851 | " \n", 852 | " \n", 853 | " \n", 854 | " \n", 855 | " \n", 856 | " \n", 857 | " \n", 858 | " \n", 859 | " \n", 860 | " \n", 861 | " \n", 862 | " \n", 863 | " \n", 864 | "
iddocumentlabel
999953793074귀신보다 사람이 얼마나 무서운가를 보여주는.. 메시지까지 담고있는 드라마~최고!1
999963025658이라크 및 아랍과의 전쟁을 그린 모든 영화 중에서 가장 최고!!1
999977698359값으로 환산할 수 없을 만큼 귀엽고 황홀한 캐릭터 ㅠㅠ1
9999870686531
999993206900파괴지왕에서 장학우 콘서트 티켓을 얻기위한... ㅋ1
\n", 865 | "
" 866 | ], 867 | "text/plain": [ 868 | " id document label\n", 869 | "99995 3793074 귀신보다 사람이 얼마나 무서운가를 보여주는.. 메시지까지 담고있는 드라마~최고! 1\n", 870 | "99996 3025658 이라크 및 아랍과의 전쟁을 그린 모든 영화 중에서 가장 최고!! 1\n", 871 | "99997 7698359 값으로 환산할 수 없을 만큼 귀엽고 황홀한 캐릭터 ㅠㅠ 1\n", 872 | "99998 7068653 짱 1\n", 873 | "99999 3206900 파괴지왕에서 장학우 콘서트 티켓을 얻기위한... ㅋ 1" 874 | ] 875 | }, 876 | "execution_count": 14, 877 | "metadata": {}, 878 | "output_type": "execute_result" 879 | } 880 | ], 881 | "source": [ 882 | "import pandas as pd\n", 883 | "\n", 884 | "random_text = pd.read_csv('ratings.txt', sep='\\t', quoting=3)\n", 885 | "random_text = random_text[random_text['label'] == 1].reset_index(drop=True)\n", 886 | "random_text.tail()" 887 | ] 888 | }, 889 | { 890 | "cell_type": "code", 891 | "execution_count": 15, 892 | "metadata": {}, 893 | "outputs": [ 894 | { 895 | "data": { 896 | "text/html": [ 897 | "
\n", 898 | "\n", 911 | "\n", 912 | " \n", 913 | " \n", 914 | " \n", 915 | " \n", 916 | " \n", 917 | " \n", 918 | " \n", 919 | " \n", 920 | " \n", 921 | " \n", 922 | " \n", 923 | " \n", 924 | " \n", 925 | " \n", 926 | " \n", 927 | " \n", 928 | " \n", 929 | " \n", 930 | " \n", 931 | " \n", 932 | " \n", 933 | " \n", 934 | " \n", 935 | " \n", 936 | " \n", 937 | " \n", 938 | " \n", 939 | " \n", 940 | " \n", 941 | " \n", 942 | " \n", 943 | " \n", 944 | " \n", 945 | " \n", 946 | "
문장혐오 여부
99995귀신보다 사람이 얼마나 무서운가를 보여주는.. 메시지까지 담고있는 드라마~최고!1
99996이라크 및 아랍과의 전쟁을 그린 모든 영화 중에서 가장 최고!!1
99997값으로 환산할 수 없을 만큼 귀엽고 황홀한 캐릭터 ㅠㅠ1
999981
99999파괴지왕에서 장학우 콘서트 티켓을 얻기위한... ㅋ1
\n", 947 | "
" 948 | ], 949 | "text/plain": [ 950 | " 문장 혐오 여부\n", 951 | "99995 귀신보다 사람이 얼마나 무서운가를 보여주는.. 메시지까지 담고있는 드라마~최고! 1\n", 952 | "99996 이라크 및 아랍과의 전쟁을 그린 모든 영화 중에서 가장 최고!! 1\n", 953 | "99997 값으로 환산할 수 없을 만큼 귀엽고 황홀한 캐릭터 ㅠㅠ 1\n", 954 | "99998 짱 1\n", 955 | "99999 파괴지왕에서 장학우 콘서트 티켓을 얻기위한... ㅋ 1" 956 | ] 957 | }, 958 | "execution_count": 15, 959 | "metadata": {}, 960 | "output_type": "execute_result" 961 | } 962 | ], 963 | "source": [ 964 | "data = {'문장':random_text['document'].tolist(), '혐오 여부':[1] * len(random_text['document'].tolist())}\n", 965 | "df_1 = pd.DataFrame(data) \n", 966 | "df_1.tail()" 967 | ] 968 | }, 969 | { 970 | "cell_type": "code", 971 | "execution_count": 19, 972 | "metadata": {}, 973 | "outputs": [ 974 | { 975 | "data": { 976 | "text/html": [ 977 | "
\n", 978 | "\n", 991 | "\n", 992 | " \n", 993 | " \n", 994 | " \n", 995 | " \n", 996 | " \n", 997 | " \n", 998 | " \n", 999 | " \n", 1000 | " \n", 1001 | " \n", 1002 | " \n", 1003 | " \n", 1004 | " \n", 1005 | " \n", 1006 | " \n", 1007 | " \n", 1008 | " \n", 1009 | " \n", 1010 | " \n", 1011 | " \n", 1012 | " \n", 1013 | " \n", 1014 | " \n", 1015 | " \n", 1016 | " \n", 1017 | " \n", 1018 | " \n", 1019 | " \n", 1020 | " \n", 1021 | " \n", 1022 | " \n", 1023 | " \n", 1024 | " \n", 1025 | " \n", 1026 | "
문장혐오 여부
189995귀신보다 사람이 얼마나 무서운가를 보여주는.. 메시지까지 담고있는 드라마~최고!1
189996이라크 및 아랍과의 전쟁을 그린 모든 영화 중에서 가장 최고!!1
189997값으로 환산할 수 없을 만큼 귀엽고 황홀한 캐릭터 ㅠㅠ1
1899981
189999파괴지왕에서 장학우 콘서트 티켓을 얻기위한... ㅋ1
\n", 1027 | "
" 1028 | ], 1029 | "text/plain": [ 1030 | " 문장 혐오 여부\n", 1031 | "189995 귀신보다 사람이 얼마나 무서운가를 보여주는.. 메시지까지 담고있는 드라마~최고! 1\n", 1032 | "189996 이라크 및 아랍과의 전쟁을 그린 모든 영화 중에서 가장 최고!! 1\n", 1033 | "189997 값으로 환산할 수 없을 만큼 귀엽고 황홀한 캐릭터 ㅠㅠ 1\n", 1034 | "189998 짱 1\n", 1035 | "189999 파괴지왕에서 장학우 콘서트 티켓을 얻기위한... ㅋ 1" 1036 | ] 1037 | }, 1038 | "execution_count": 19, 1039 | "metadata": {}, 1040 | "output_type": "execute_result" 1041 | } 1042 | ], 1043 | "source": [ 1044 | "df= pd.concat([df_0, df_1])\n", 1045 | "df.reset_index(drop=True).tail()" 1046 | ] 1047 | }, 1048 | { 1049 | "cell_type": "code", 1050 | "execution_count": 21, 1051 | "metadata": {}, 1052 | "outputs": [ 1053 | { 1054 | "data": { 1055 | "text/html": [ 1056 | "
\n", 1057 | "\n", 1070 | "\n", 1071 | " \n", 1072 | " \n", 1073 | " \n", 1074 | " \n", 1075 | " \n", 1076 | " \n", 1077 | " \n", 1078 | " \n", 1079 | " \n", 1080 | " \n", 1081 | " \n", 1082 | " \n", 1083 | " \n", 1084 | " \n", 1085 | " \n", 1086 | " \n", 1087 | " \n", 1088 | " \n", 1089 | " \n", 1090 | " \n", 1091 | " \n", 1092 | " \n", 1093 | " \n", 1094 | " \n", 1095 | " \n", 1096 | " \n", 1097 | " \n", 1098 | " \n", 1099 | " \n", 1100 | " \n", 1101 | " \n", 1102 | " \n", 1103 | " \n", 1104 | " \n", 1105 | "
문장혐오 여부
189995원작을 읽을 때 이런 건 절대 영상화하기 힘들다고 생각했는데 벤휘쇼의 연기와 더불어...1
189996케석대 어깨 올라간거봐라 ㅋㅋ0
189997@김짜꾸 day and night\\n\\nround the clock\\n\\nwitho...0
189998로버트다우니주니어를 좋아해서 봤는데너무재밌게 봤던영화생각없이 볼때 딱좋음1
189999@익명_146173 개지랄병 병신좌좀새끼ㅋㅋㅋㅋ0
\n", 1106 | "
" 1107 | ], 1108 | "text/plain": [ 1109 | " 문장 혐오 여부\n", 1110 | "189995 원작을 읽을 때 이런 건 절대 영상화하기 힘들다고 생각했는데 벤휘쇼의 연기와 더불어... 1\n", 1111 | "189996 케석대 어깨 올라간거봐라 ㅋㅋ 0\n", 1112 | "189997 @김짜꾸 day and night\\n\\nround the clock\\n\\nwitho... 0\n", 1113 | "189998 로버트다우니주니어를 좋아해서 봤는데너무재밌게 봤던영화생각없이 볼때 딱좋음 1\n", 1114 | "189999 @익명_146173 개지랄병 병신좌좀새끼ㅋㅋㅋㅋ 0" 1115 | ] 1116 | }, 1117 | "execution_count": 21, 1118 | "metadata": {}, 1119 | "output_type": "execute_result" 1120 | } 1121 | ], 1122 | "source": [ 1123 | "import pandas as pd \n", 1124 | "from sklearn.utils import shuffle\n", 1125 | "\n", 1126 | "dataframe = shuffle(df).reset_index(drop=True)\n", 1127 | "dataframe.tail()" 1128 | ] 1129 | }, 1130 | { 1131 | "cell_type": "code", 1132 | "execution_count": 22, 1133 | "metadata": {}, 1134 | "outputs": [ 1135 | { 1136 | "data": { 1137 | "text/plain": [ 1138 | "1 100000\n", 1139 | "0 90000\n", 1140 | "Name: 혐오 여부, dtype: int64" 1141 | ] 1142 | }, 1143 | "execution_count": 22, 1144 | "metadata": {}, 1145 | "output_type": "execute_result" 1146 | } 1147 | ], 1148 | "source": [ 1149 | "dataframe['혐오 여부'].value_counts()" 1150 | ] 1151 | }, 1152 | { 1153 | "cell_type": "code", 1154 | "execution_count": 23, 1155 | "metadata": {}, 1156 | "outputs": [], 1157 | "source": [ 1158 | "dataframe.to_csv('hate_speech_binary_dataset.csv', index=False)" 1159 | ] 1160 | }, 1161 | { 1162 | "cell_type": "code", 1163 | "execution_count": 24, 1164 | "metadata": {}, 1165 | "outputs": [ 1166 | { 1167 | "data": { 1168 | "text/plain": [ 1169 | "1 100000\n", 1170 | "0 90000\n", 1171 | "Name: 혐오 여부, dtype: int64" 1172 | ] 1173 | }, 1174 | "execution_count": 24, 1175 | "metadata": {}, 1176 | "output_type": "execute_result" 1177 | } 1178 | ], 1179 | "source": [ 1180 | "import pandas as pd \n", 1181 | "from sklearn.utils import shuffle\n", 1182 | "\n", 1183 | "\n", 1184 | "df = shuffle(pd.read_csv('hate_speech_binary_dataset.csv', index_col=0)).reset_index(drop=True)\n", 1185 | "df.to_csv(dataset_dir)\n", 1186 | "df['혐오 여부'].value_counts()" 1187 | ] 1188 | }, 1189 | { 1190 | "cell_type": "code", 1191 | "execution_count": null, 1192 | "metadata": { 1193 | "scrolled": true 1194 | }, 1195 | "outputs": [], 1196 | "source": [ 1197 | "import pandas as pd\n", 1198 | "\n", 1199 | "hate_text = pd.read_csv('hate_speech_data_cleaned.csv')\n", 1200 | "hate_text_0 = hate_text[hate_text['topic'] == 0].reset_index(drop=True)\n", 1201 | "hate_text_1 = hate_text[hate_text['topic'] == 1].reset_index(drop=True)\n", 1202 | "hate_text_2 = hate_text[hate_text['topic'] == 2].reset_index(drop=True)\n", 1203 | "hate_text_3 = hate_text[hate_text['topic'] == 3].reset_index(drop=True)\n", 1204 | "random_text = random_text[random_text['label'] == 1].reset_index(drop=True)\n", 1205 | "\n", 1206 | "dataset_dir = 'hate_speech_dataset.csv'\n", 1207 | "dataframe = pd.DataFrame(columns=['문장', '혐오 여부'])\n", 1208 | "dataframe.to_csv(dataset_dir, index=False)\n", 1209 | "dataframe = pd.read_csv(dataset_dir)\n", 1210 | "\n", 1211 | "count = 0 \n", 1212 | "\n", 1213 | "for q in hate_text_0['hate_speech'][:6000]:\n", 1214 | " data = pd.DataFrame({'문장': q, '혐오 여부': 0}, index=[0])\n", 1215 | " dataframe = dataframe.append(data, ignore_index=True)\n", 1216 | " dataframe.drop_duplicates().dropna().reset_index(drop=True).to_csv(dataset_dir)\n", 1217 | " count = count+1\n", 1218 | " print(count)\n", 1219 | " \n", 1220 | "for w in hate_text_1['hate_speech'][:6000]:\n", 1221 | " data = pd.DataFrame({'문장': w, '혐오 여부':1}, index=[0])\n", 1222 | " dataframe = dataframe.append(data, ignore_index=True)\n", 1223 | " dataframe.drop_duplicates().dropna().reset_index(drop=True).to_csv(dataset_dir)\n", 1224 | " count = count+1\n", 1225 | " print(count)\n", 1226 | "\n", 1227 | "for e in hate_text_2['hate_speech']:#[:1900]:\n", 1228 | " data = pd.DataFrame({'문장': e, '혐오 여부': 2}, index=[0])\n", 1229 | " dataframe = dataframe.append(data, ignore_index=True)\n", 1230 | " dataframe.drop_duplicates().dropna().reset_index(drop=True).to_csv(dataset_dir)\n", 1231 | " count = count+1\n", 1232 | " print(count)\n", 1233 | " \n", 1234 | "for r in hate_text_3['hate_speech']:#[:1900]:\n", 1235 | " data = pd.DataFrame({'문장': r, '혐오 여부': 3}, index=[0])\n", 1236 | " dataframe = dataframe.append(data, ignore_index=True)\n", 1237 | " dataframe.drop_duplicates().dropna().reset_index(drop=True).to_csv(dataset_dir)\n", 1238 | " count = count+1\n", 1239 | " print(count)\n", 1240 | "\n", 1241 | "for t in random_text['document'][:8000]:\n", 1242 | " data = pd.DataFrame({'문장': t, '혐오 여부': 4}, index=[0])\n", 1243 | " dataframe = dataframe.append(data, ignore_index=True)\n", 1244 | " dataframe.drop_duplicates().dropna().reset_index(drop=True).to_csv(dataset_dir)\n", 1245 | " count = count+1\n", 1246 | " print(count)\n" 1247 | ] 1248 | }, 1249 | { 1250 | "cell_type": "code", 1251 | "execution_count": 5, 1252 | "metadata": {}, 1253 | "outputs": [ 1254 | { 1255 | "data": { 1256 | "text/html": [ 1257 | "
\n", 1258 | "\n", 1271 | "\n", 1272 | " \n", 1273 | " \n", 1274 | " \n", 1275 | " \n", 1276 | " \n", 1277 | " \n", 1278 | " \n", 1279 | " \n", 1280 | " \n", 1281 | " \n", 1282 | " \n", 1283 | " \n", 1284 | " \n", 1285 | " \n", 1286 | " \n", 1287 | " \n", 1288 | " \n", 1289 | " \n", 1290 | " \n", 1291 | " \n", 1292 | " \n", 1293 | " \n", 1294 | " \n", 1295 | " \n", 1296 | " \n", 1297 | " \n", 1298 | " \n", 1299 | " \n", 1300 | " \n", 1301 | " \n", 1302 | " \n", 1303 | " \n", 1304 | " \n", 1305 | " \n", 1306 | "
문장혐오 여부
31502일게이 퀄리티 살아잇노1
31503제가 서부 영화를 좋아하는 데, 짱이네요.4
31504최고의 영화!! 말이 필요없습니다.4
31505관심주지마라1
31506아무도 부정할 수 없는 우리네 모습4
\n", 1307 | "
" 1308 | ], 1309 | "text/plain": [ 1310 | " 문장 혐오 여부\n", 1311 | "31502 일게이 퀄리티 살아잇노 1\n", 1312 | "31503 제가 서부 영화를 좋아하는 데, 짱이네요. 4\n", 1313 | "31504 최고의 영화!! 말이 필요없습니다. 4\n", 1314 | "31505 관심주지마라 1\n", 1315 | "31506 아무도 부정할 수 없는 우리네 모습 4" 1316 | ] 1317 | }, 1318 | "execution_count": 5, 1319 | "metadata": {}, 1320 | "output_type": "execute_result" 1321 | } 1322 | ], 1323 | "source": [ 1324 | "import pandas as pd \n", 1325 | "from sklearn.utils import shuffle\n", 1326 | "\n", 1327 | "dataset_dir = 'hate_speech_dataset.csv'\n", 1328 | "df = shuffle(pd.read_csv('hate_speech_dataset.csv', index_col=0)).reset_index(drop=True)\n", 1329 | "df.to_csv(dataset_dir)\n", 1330 | "df.tail()" 1331 | ] 1332 | }, 1333 | { 1334 | "cell_type": "code", 1335 | "execution_count": 6, 1336 | "metadata": {}, 1337 | "outputs": [ 1338 | { 1339 | "data": { 1340 | "text/html": [ 1341 | "
\n", 1342 | "\n", 1355 | "\n", 1356 | " \n", 1357 | " \n", 1358 | " \n", 1359 | " \n", 1360 | " \n", 1361 | " \n", 1362 | " \n", 1363 | " \n", 1364 | " \n", 1365 | " \n", 1366 | " \n", 1367 | " \n", 1368 | " \n", 1369 | " \n", 1370 | " \n", 1371 | " \n", 1372 | " \n", 1373 | " \n", 1374 | " \n", 1375 | " \n", 1376 | " \n", 1377 | " \n", 1378 | " \n", 1379 | " \n", 1380 | " \n", 1381 | " \n", 1382 | " \n", 1383 | " \n", 1384 | " \n", 1385 | " \n", 1386 | " \n", 1387 | " \n", 1388 | " \n", 1389 | " \n", 1390 | " \n", 1391 | " \n", 1392 | " \n", 1393 | " \n", 1394 | " \n", 1395 | " \n", 1396 | "
iddocumentlabel
999953793074귀신보다 사람이 얼마나 무서운가를 보여주는.. 메시지까지 담고있는 드라마~최고!1
999963025658이라크 및 아랍과의 전쟁을 그린 모든 영화 중에서 가장 최고!!1
999977698359값으로 환산할 수 없을 만큼 귀엽고 황홀한 캐릭터 ㅠㅠ1
9999870686531
999993206900파괴지왕에서 장학우 콘서트 티켓을 얻기위한... ㅋ1
\n", 1397 | "
" 1398 | ], 1399 | "text/plain": [ 1400 | " id document label\n", 1401 | "99995 3793074 귀신보다 사람이 얼마나 무서운가를 보여주는.. 메시지까지 담고있는 드라마~최고! 1\n", 1402 | "99996 3025658 이라크 및 아랍과의 전쟁을 그린 모든 영화 중에서 가장 최고!! 1\n", 1403 | "99997 7698359 값으로 환산할 수 없을 만큼 귀엽고 황홀한 캐릭터 ㅠㅠ 1\n", 1404 | "99998 7068653 짱 1\n", 1405 | "99999 3206900 파괴지왕에서 장학우 콘서트 티켓을 얻기위한... ㅋ 1" 1406 | ] 1407 | }, 1408 | "execution_count": 6, 1409 | "metadata": {}, 1410 | "output_type": "execute_result" 1411 | } 1412 | ], 1413 | "source": [ 1414 | "import pandas as pd\n", 1415 | "\n", 1416 | "random_text = pd.read_csv('ratings.txt', sep='\\t', quoting=3)\n", 1417 | "random_text = random_text[random_text['label'] == 1].reset_index(drop=True)\n", 1418 | "random_text = random_text[]" 1419 | ] 1420 | }, 1421 | { 1422 | "cell_type": "code", 1423 | "execution_count": 2, 1424 | "metadata": {}, 1425 | "outputs": [], 1426 | "source": [ 1427 | "import pandas as pd\n", 1428 | "\n", 1429 | "data = pd.read_csv('hate_speech_topic_dataset.csv', index_col=0)\n", 1430 | "data[data['혐오 여부']==0].reset_index(drop=True).to_csv('hate_speech_topic_region.csv', index=False)" 1431 | ] 1432 | }, 1433 | { 1434 | "cell_type": "markdown", 1435 | "metadata": {}, 1436 | "source": [ 1437 | "### 3.2. Recurrent Neural Network" 1438 | ] 1439 | }, 1440 | { 1441 | "cell_type": "code", 1442 | "execution_count": null, 1443 | "metadata": {}, 1444 | "outputs": [], 1445 | "source": [ 1446 | "from keras import Input, Model\n", 1447 | "from keras.layers import Embedding, Dense, Dropout, LSTM\n", 1448 | "\n", 1449 | "\n", 1450 | "class TextRNN(object):\n", 1451 | " def __init__(self, maxlen, max_features, embedding_dims,\n", 1452 | " class_num=2,\n", 1453 | " last_activation='softmax'):\n", 1454 | " self.maxlen = maxlen\n", 1455 | " self.max_features = max_features\n", 1456 | " self.embedding_dims = embedding_dims\n", 1457 | " self.class_num = class_num\n", 1458 | " self.last_activation = last_activation\n", 1459 | "\n", 1460 | " def get_model(self):\n", 1461 | " input = Input((self.maxlen,))\n", 1462 | "\n", 1463 | " embedding = Embedding(self.max_features, self.embedding_dims, input_length=self.maxlen)(input)\n", 1464 | " x = LSTM(128)(embedding) # LSTM or GRU\n", 1465 | "\n", 1466 | " output = Dense(self.class_num, activation=self.last_activation)(x)\n", 1467 | " model = Model(inputs=input, outputs=output)\n", 1468 | " return model\n" 1469 | ] 1470 | }, 1471 | { 1472 | "cell_type": "code", 1473 | "execution_count": 2, 1474 | "metadata": {}, 1475 | "outputs": [ 1476 | { 1477 | "name": "stdout", 1478 | "output_type": "stream", 1479 | "text": [ 1480 | "Model: \"model_1\"\n", 1481 | "_________________________________________________________________\n", 1482 | "Layer (type) Output Shape Param # \n", 1483 | "=================================================================\n", 1484 | "input_1 (InputLayer) (None, 128) 0 \n", 1485 | "_________________________________________________________________\n", 1486 | "embedding_1 (Embedding) (None, 128, 50) 250000 \n", 1487 | "_________________________________________________________________\n", 1488 | "lstm_1 (LSTM) (None, 128) 91648 \n", 1489 | "_________________________________________________________________\n", 1490 | "dense_1 (Dense) (None, 2) 258 \n", 1491 | "=================================================================\n", 1492 | "Total params: 341,906\n", 1493 | "Trainable params: 341,906\n", 1494 | "Non-trainable params: 0\n", 1495 | "_________________________________________________________________\n" 1496 | ] 1497 | } 1498 | ], 1499 | "source": [ 1500 | "import pandas as pd\n", 1501 | "import numpy as np\n", 1502 | "from keras.callbacks import EarlyStopping\n", 1503 | "from keras.preprocessing import sequence\n", 1504 | "from keras.preprocessing.text import Tokenizer\n", 1505 | "from sklearn.preprocessing import OneHotEncoder\n", 1506 | "from sklearn.model_selection import train_test_split\n", 1507 | "\n", 1508 | "max_features = 5000\n", 1509 | "maxlen = 128\n", 1510 | "batch_size = 64\n", 1511 | "embedding_dims = 50\n", 1512 | "epochs = 100\n", 1513 | "\n", 1514 | "data = pd.read_csv('hate_speech_binary_dataset.csv').dropna().reset_index(drop=True)\n", 1515 | "data[\"문장\"] = data[\"문장\"].astype('string')\n", 1516 | "\n", 1517 | "x_train,x_test,y_train,y_test = train_test_split(data[\"문장\"], data[\"혐오 여부\"],test_size=0.15)\n", 1518 | "\n", 1519 | "tokenizer = Tokenizer(num_words=5000)\n", 1520 | "tokenizer.fit_on_texts(data[\"문장\"])\n", 1521 | "\n", 1522 | "x_train = tokenizer.texts_to_sequences(x_train)\n", 1523 | "x_test = tokenizer.texts_to_sequences(x_test)\n", 1524 | "\n", 1525 | "x_train = sequence.pad_sequences(x_train, maxlen=maxlen)\n", 1526 | "x_test = sequence.pad_sequences(x_test, maxlen=maxlen)\n", 1527 | "\n", 1528 | "encoder = OneHotEncoder()\n", 1529 | "\n", 1530 | "y_train = encoder.fit_transform(np.asarray(y_train).reshape(-1,1))\n", 1531 | "y_test= encoder.fit_transform(np.asarray(y_test).reshape(-1,1))\n", 1532 | "\n", 1533 | "model = TextRNN(maxlen, max_features, embedding_dims).get_model()\n", 1534 | "model.summary()" 1535 | ] 1536 | }, 1537 | { 1538 | "cell_type": "code", 1539 | "execution_count": 3, 1540 | "metadata": {}, 1541 | "outputs": [ 1542 | { 1543 | "name": "stdout", 1544 | "output_type": "stream", 1545 | "text": [ 1546 | "WARNING:tensorflow:From C:\\Users\\nemo\\Anaconda3\\envs\\hate_speech_topic\\lib\\site-packages\\keras\\backend\\tensorflow_backend.py:422: The name tf.global_variables is deprecated. Please use tf.compat.v1.global_variables instead.\n", 1547 | "\n", 1548 | "Train on 161495 samples, validate on 28500 samples\n", 1549 | "Epoch 1/100\n", 1550 | "161495/161495 [==============================] - 392s 2ms/step - loss: 0.3001 - accuracy: 0.8514 - val_loss: 0.2812 - val_accuracy: 0.8609\n", 1551 | "Epoch 2/100\n", 1552 | "161495/161495 [==============================] - 418s 3ms/step - loss: 0.2637 - accuracy: 0.8679 - val_loss: 0.2767 - val_accuracy: 0.8646\n", 1553 | "Epoch 3/100\n", 1554 | "161495/161495 [==============================] - 431s 3ms/step - loss: 0.2541 - accuracy: 0.8720 - val_loss: 0.2766 - val_accuracy: 0.8644\n", 1555 | "Epoch 4/100\n", 1556 | "161495/161495 [==============================] - 419s 3ms/step - loss: 0.2456 - accuracy: 0.8755 - val_loss: 0.2824 - val_accuracy: 0.8648\n", 1557 | "Epoch 5/100\n", 1558 | "161495/161495 [==============================] - 405s 3ms/step - loss: 0.2374 - accuracy: 0.8789 - val_loss: 0.2869 - val_accuracy: 0.8630\n", 1559 | "Epoch 6/100\n", 1560 | "161495/161495 [==============================] - 401s 2ms/step - loss: 0.2289 - accuracy: 0.8816 - val_loss: 0.2941 - val_accuracy: 0.8635\n", 1561 | "Epoch 7/100\n", 1562 | "161495/161495 [==============================] - 405s 3ms/step - loss: 0.2210 - accuracy: 0.8850 - val_loss: 0.3076 - val_accuracy: 0.8611\n" 1563 | ] 1564 | }, 1565 | { 1566 | "data": { 1567 | "text/plain": [ 1568 | "" 1569 | ] 1570 | }, 1571 | "execution_count": 3, 1572 | "metadata": {}, 1573 | "output_type": "execute_result" 1574 | } 1575 | ], 1576 | "source": [ 1577 | "model.compile('adam', 'categorical_crossentropy', metrics=['accuracy'])\n", 1578 | "\n", 1579 | "early_stopping = EarlyStopping(monitor='val_accuracy', patience=3, mode='max')\n", 1580 | "model.fit(x_train, y_train,\n", 1581 | " batch_size=batch_size,\n", 1582 | " epochs=epochs,\n", 1583 | " callbacks=[early_stopping],\n", 1584 | " validation_data=(x_test, y_test))" 1585 | ] 1586 | } 1587 | ], 1588 | "metadata": { 1589 | "kernelspec": { 1590 | "display_name": "Python [conda env:hate_speech_topic]", 1591 | "language": "python", 1592 | "name": "conda-env-hate_speech_topic-py" 1593 | }, 1594 | "language_info": { 1595 | "codemirror_mode": { 1596 | "name": "ipython", 1597 | "version": 3 1598 | }, 1599 | "file_extension": ".py", 1600 | "mimetype": "text/x-python", 1601 | "name": "python", 1602 | "nbconvert_exporter": "python", 1603 | "pygments_lexer": "ipython3", 1604 | "version": "3.7.7" 1605 | } 1606 | }, 1607 | "nbformat": 4, 1608 | "nbformat_minor": 2 1609 | } 1610 | --------------------------------------------------------------------------------