Skip to content

Commit ce88861

Browse files
authored
Merge pull request #6 from atrifat/feat-support-custom-and-hybrid-model
Support custom and hybrid model (Fast Inference for CPU-only device)
2 parents 46de22e + 1930949 commit ce88861

File tree

5 files changed

+121
-10
lines changed

5 files changed

+121
-10
lines changed

.env.example

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,19 @@ ENABLE_CACHE=false
1313
CACHE_DURATION_SECONDS=60
1414

1515
# (Optional. Default: auto. Options: auto,cpu,cuda) Set torch default device for detoxify library. Automatically detect if cuda/gpu device is available
16-
TORCH_DEVICE=auto
16+
TORCH_DEVICE=auto
17+
18+
# (Required. Default: detoxify. Options: detoxify, hybrid, custom). Set classification model to be used in prediction. "hybrid" and "custom" can be used as fast model if application run on machine without GPU.
19+
HATE_SPEECH_MODEL=detoxify
20+
21+
# (Optional. Default: "". Options: "any potential words") Some potential toxic words can be included to assist hybrid model detection. Hybrid approach uses both custom and detoxify model based on probability thresold.
22+
# POTENTIAL_TOXIC_WORDS="f**k,n***a,ni**er"
23+
24+
# (Optional. Default: 0.5 . Options: float value between 0.0 and 1.0) Probability thresold when using "hybrid" model. The thresold will determine whether to continue classify using detoxify model after using custom model
25+
HYBRID_THRESOLD_CHECK=0.5
26+
27+
# (Optional. Default: "./experiments/model_voting_partial_best.pkl") Custom model path. Custom Pretrained model (pickle) of scikit-learn which implement predict_proba
28+
# CUSTOM_MODEL_PATH="./experiments/model_voting_partial_best.pkl"
29+
30+
# (Optional. Default: "./experiments/vectorizer_count_no_stop_words.pkl") Custom vectorizer path. Custom Vectorizer model (pickle) of scikit-learn which implement vector transform for text
31+
# CUSTOM_VECTORIZER_PATH="./experiments/vectorizer_count_no_stop_words.pkl"

Dockerfile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ ENV HOME=/home/user
2626

2727
COPY --from=builder --chown=user:user /builder/venv /app/venv
2828

29-
COPY --chown=user:user app.py app.py
29+
COPY --chown=user:user app.py app.py
30+
31+
RUN mkdir -p /app/experiments
32+
COPY --chown=user:user experiments/*.pkl experiments/
3033

3134
RUN chown -R user:user /app && chown -R user:user /home/user
3235

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
# hate-speech-detector-api
22

3-
A Simple PoC (Proof of Concept) of Hate-speech (Toxic content) Detector API Server using model from [detoxify](https://github.com/unitaryai/detoxify). Detoxify (unbiased model) achieves score of 93.74% compared to top leaderboard score with 94.73% in [Jigsaw Unintended Bias in Toxicity Classification](https://www.kaggle.com/c/jigsaw-unintended-bias-in-toxicity-classification).
3+
A Simple PoC (Proof of Concept) of Hate-speech (Toxic content) Detector API Server using model from [detoxify](https://github.com/unitaryai/detoxify) and/or [custom traditional machine learning](experiments/model_voting_partial_best.pkl) model. Detoxify (unbiased model) achieves AUC score of 93.74% compared to top leaderboard score with AUC 94.73% in [Jigsaw Unintended Bias in Toxicity Classification](https://www.kaggle.com/c/jigsaw-unintended-bias-in-toxicity-classification). To those who are interested in training custom machine learning model based on [Jigsaw Unintended Bias in Toxicity Classification](https://www.kaggle.com/c/jigsaw-unintended-bias-in-toxicity-classification) can take a look at our [Jupyter Notebook](experiments/hate-speech-classification.ipynb).
4+
5+
hate-speech-detector-api is a core dependency of [nostr-filter-relay](https://github.com/atrifat/nostr-filter-relay).
46

57
## Demo
68

7-
A demo instance is available on [HuggingFace Spaces - https://atrifat-hate-speech-detector-api-demo.hf.space](https://atrifat-hate-speech-detector-api-demo.hf.space). There is no guarantee for the uptime, but feel free to test.
9+
A demo gradio showcase is available on [HuggingFace Spaces - https://huggingface.co/spaces/rifatramadhani/hate-speech-detector](https://huggingface.co/spaces/rifatramadhani/hate-speech-detector). There is no guarantee for the uptime, but feel free to test.
810

911
## Requirements
1012

app.py

Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44
from flask import Flask, request, jsonify
55
import functools
66
import datetime
7-
import torch
87
from detoxify import Detoxify
98
import logging
9+
import torch
1010
from flask_caching import Cache
11+
import pickle
1112

1213
load_dotenv()
1314

@@ -16,11 +17,27 @@
1617
APP_ENV = os.getenv("APP_ENV", "production")
1718
LISTEN_HOST = os.getenv("LISTEN_HOST", "0.0.0.0")
1819
LISTEN_PORT = os.getenv("LISTEN_PORT", "7860")
20+
21+
CUSTOM_MODEL_PATH = os.getenv(
22+
"CUSTOM_MODEL_PATH",
23+
os.path.dirname(os.path.abspath(__file__))
24+
+ "/experiments/model_voting_partial_best.pkl",
25+
)
26+
CUSTOM_VECTORIZER_PATH = os.getenv(
27+
"CUSTOM_VECTORIZER_PATH",
28+
os.path.dirname(os.path.abspath(__file__))
29+
+ "/experiments/vectorizer_count_no_stop_words.pkl",
30+
)
31+
1932
DETOXIFY_MODEL = os.getenv("DETOXIFY_MODEL", "unbiased-small")
33+
HATE_SPEECH_MODEL = os.getenv("HATE_SPEECH_MODEL", "detoxify")
2034
CACHE_DURATION_SECONDS = int(os.getenv("CACHE_DURATION_SECONDS", 60))
2135
ENABLE_CACHE = os.getenv("ENABLE_CACHE", "false") == "true"
36+
POTENTIAL_TOXIC_WORDS = list(
37+
filter(None, os.getenv("POTENTIAL_TOXIC_WORDS", "").split(","))
38+
)
39+
HYBRID_THRESOLD_CHECK = float(os.getenv("HYBRID_THRESOLD_CHECK", 0.5))
2240
TORCH_DEVICE = os.getenv("TORCH_DEVICE", "auto")
23-
2441
APP_VERSION = "0.2.0"
2542

2643
# Setup logging configuration
@@ -47,7 +64,22 @@
4764
else:
4865
torch_device = TORCH_DEVICE
4966

50-
model = Detoxify(DETOXIFY_MODEL, device=torch_device)
67+
if HATE_SPEECH_MODEL in ["hybrid", "detoxify"]:
68+
model = Detoxify(DETOXIFY_MODEL, device=torch_device)
69+
70+
if HATE_SPEECH_MODEL in ["hybrid", "custom"]:
71+
try:
72+
with open(CUSTOM_VECTORIZER_PATH, "rb") as f:
73+
vectorizer = pickle.load(f)
74+
except Exception as e:
75+
vectorizer = None
76+
77+
try:
78+
with open(CUSTOM_MODEL_PATH, "rb") as f:
79+
model_custom = pickle.load(f)
80+
except Exception as e:
81+
raise e
82+
5183

5284
app = Flask(__name__)
5385

@@ -107,6 +139,55 @@ def perform_hate_speech_analysis(query):
107139
return result
108140

109141

142+
def perform_hate_speech_analysis_custom(query):
143+
query_vector = vectorizer.transform([query]) if vectorizer != None else ["query"]
144+
145+
result = {
146+
"identity_attack": 0.0,
147+
"insult": 0.0,
148+
"obscene": 0.0,
149+
"severe_toxicity": 0.0,
150+
"sexual_explicit": 0.0,
151+
"threat": 0.0,
152+
"toxicity": 0.0,
153+
}
154+
155+
temp_result = model_custom.predict_proba(query_vector)
156+
result["toxicity"] = temp_result[0][1].round(3).astype("float")
157+
158+
return result
159+
160+
161+
def perform_hate_speech_analysis_hybrid(
162+
query, thresold_check=0.5, potential_toxic_words=[]
163+
):
164+
result = {
165+
"identity_attack": 0.0,
166+
"insult": 0.0,
167+
"obscene": 0.0,
168+
"severe_toxicity": 0.0,
169+
"sexual_explicit": 0.0,
170+
"threat": 0.0,
171+
"toxicity": 0.0,
172+
}
173+
temp_result_custom = perform_hate_speech_analysis_custom(query)
174+
175+
has_potential_toxic_word = False
176+
for word in potential_toxic_words:
177+
if word in query:
178+
has_potential_toxic_word = True
179+
break
180+
181+
if temp_result_custom["toxicity"] > thresold_check or has_potential_toxic_word:
182+
temp_result_detoxify = perform_hate_speech_analysis(query)
183+
if temp_result_detoxify["toxicity"] > thresold_check:
184+
result = temp_result_detoxify
185+
else:
186+
result = temp_result_custom
187+
188+
return result
189+
190+
110191
@app.errorhandler(Exception)
111192
def handle_exception(error):
112193
res = {"error": str(error)}
@@ -120,7 +201,16 @@ def predict():
120201
data = request.json
121202
q = data["q"]
122203
start_time = datetime.datetime.now()
123-
result = perform_hate_speech_analysis(q)
204+
205+
if HATE_SPEECH_MODEL == "custom":
206+
result = perform_hate_speech_analysis_custom(q)
207+
elif HATE_SPEECH_MODEL == "hybrid":
208+
result = perform_hate_speech_analysis_hybrid(
209+
q, HYBRID_THRESOLD_CHECK, POTENTIAL_TOXIC_WORDS
210+
)
211+
else:
212+
result = perform_hate_speech_analysis(q)
213+
124214
end_time = datetime.datetime.now()
125215
elapsed_time = end_time - start_time
126216
logging.debug("elapsed predict time: %s", str(elapsed_time))

requirements.txt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ detoxify==0.5.1
22
Flask==3.0.0
33
Flask-Caching==2.3.0
44
gunicorn==21.2.0
5-
pandas==2.1.1
6-
python-dotenv==1.0.0
5+
pandas==2.2.2
6+
python-dotenv==1.0.0
7+
scikit-learn==1.5.0

0 commit comments

Comments
 (0)