Spaces:
Running
on
Zero
Running
on
Zero
update
Browse files- app.py +6 -7
- binoculars/__init__.py +3 -0
- binoculars/__pycache__/__init__.cpython-310.pyc +0 -0
- binoculars/__pycache__/deepseek_detector.cpython-310.pyc +0 -0
- binoculars/__pycache__/detector.cpython-310.pyc +0 -0
- binoculars/__pycache__/llama_detector.cpython-310.pyc +0 -0
- binoculars/__pycache__/metrics.cpython-310.pyc +0 -0
- binoculars/__pycache__/utils.cpython-310.pyc +0 -0
- binoculars/detector.py +117 -0
- binoculars/metrics.py +57 -0
- binoculars/utils.py +16 -0
- binoculars_utils.py +43 -0
- demo/binary_classifier_demo.py +191 -0
- ex_text.txt +3 -0
- feature_extraction.py +76 -0
- main.py +66 -0
- model_utils.py +96 -0
- models/medium_binary_classifier/cv_results.json +71 -0
- models/medium_binary_classifier/imputer.joblib +3 -0
- models/medium_binary_classifier/label_encoder.joblib +3 -0
- models/medium_binary_classifier/nn_model.pt +3 -0
- models/medium_binary_classifier/scaler.joblib +3 -0
- requirements.txt +10 -0
- setup.py +15 -0
app.py
CHANGED
@@ -1,7 +1,6 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
demo.launch()
|
|
|
1 |
+
from demo.binary_classifier_demo import binary_app
|
2 |
+
|
3 |
+
if __name__ == "__main__":
|
4 |
+
# Launch only the binary classifier demo
|
5 |
+
print("Starting Binary Classifier demo...")
|
6 |
+
binary_app.launch(show_api=False, debug=True, share=True)
|
|
binoculars/__init__.py
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
from .detector import Binoculars
|
2 |
+
|
3 |
+
__all__ = ["Binoculars"]
|
binoculars/__pycache__/__init__.cpython-310.pyc
ADDED
Binary file (202 Bytes). View file
|
|
binoculars/__pycache__/deepseek_detector.cpython-310.pyc
ADDED
Binary file (3.83 kB). View file
|
|
binoculars/__pycache__/detector.cpython-310.pyc
ADDED
Binary file (3.82 kB). View file
|
|
binoculars/__pycache__/llama_detector.cpython-310.pyc
ADDED
Binary file (3.59 kB). View file
|
|
binoculars/__pycache__/metrics.cpython-310.pyc
ADDED
Binary file (1.8 kB). View file
|
|
binoculars/__pycache__/utils.cpython-310.pyc
ADDED
Binary file (688 Bytes). View file
|
|
binoculars/detector.py
ADDED
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Union
|
2 |
+
|
3 |
+
import os
|
4 |
+
import numpy as np
|
5 |
+
import torch
|
6 |
+
import transformers
|
7 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
8 |
+
|
9 |
+
from .utils import assert_tokenizer_consistency
|
10 |
+
from .metrics import perplexity, entropy
|
11 |
+
|
12 |
+
torch.set_grad_enabled(False)
|
13 |
+
|
14 |
+
huggingface_config = {
|
15 |
+
# Only required for private models from Huggingface (e.g. LLaMA models)
|
16 |
+
"TOKEN": os.environ.get("HF_TOKEN", None)
|
17 |
+
}
|
18 |
+
|
19 |
+
# selected using Falcon-7B and Falcon-7B-Instruct at bfloat16
|
20 |
+
BINOCULARS_ACCURACY_THRESHOLD = 0.9015310749276843 # optimized for f1-score
|
21 |
+
BINOCULARS_FPR_THRESHOLD = 0.8536432310785527 # optimized for low-fpr [chosen at 0.01%]
|
22 |
+
|
23 |
+
DEVICE_1 = "cuda:0" if torch.cuda.is_available() else "cpu"
|
24 |
+
DEVICE_2 = "cuda:1" if torch.cuda.device_count() > 1 else DEVICE_1
|
25 |
+
|
26 |
+
|
27 |
+
class Binoculars(object):
|
28 |
+
def __init__(self,
|
29 |
+
observer_name_or_path: str = "tiiuae/falcon-7b",
|
30 |
+
performer_name_or_path: str = "tiiuae/falcon-7b-instruct",
|
31 |
+
use_bfloat16: bool = True,
|
32 |
+
max_token_observed: int = 512,
|
33 |
+
mode: str = "low-fpr",
|
34 |
+
) -> None:
|
35 |
+
assert_tokenizer_consistency(observer_name_or_path, performer_name_or_path)
|
36 |
+
|
37 |
+
self.change_mode(mode)
|
38 |
+
self.observer_model = AutoModelForCausalLM.from_pretrained(observer_name_or_path,
|
39 |
+
device_map={"": DEVICE_1},
|
40 |
+
trust_remote_code=True,
|
41 |
+
torch_dtype=torch.bfloat16 if use_bfloat16
|
42 |
+
else torch.float32,
|
43 |
+
token=huggingface_config["TOKEN"]
|
44 |
+
)
|
45 |
+
self.performer_model = AutoModelForCausalLM.from_pretrained(performer_name_or_path,
|
46 |
+
device_map={"": DEVICE_2},
|
47 |
+
trust_remote_code=True,
|
48 |
+
torch_dtype=torch.bfloat16 if use_bfloat16
|
49 |
+
else torch.float32,
|
50 |
+
token=huggingface_config["TOKEN"]
|
51 |
+
)
|
52 |
+
self.observer_model.eval()
|
53 |
+
self.performer_model.eval()
|
54 |
+
|
55 |
+
self.tokenizer = AutoTokenizer.from_pretrained(observer_name_or_path)
|
56 |
+
if not self.tokenizer.pad_token:
|
57 |
+
self.tokenizer.pad_token = self.tokenizer.eos_token
|
58 |
+
self.max_token_observed = max_token_observed
|
59 |
+
|
60 |
+
def change_mode(self, mode: str) -> None:
|
61 |
+
if mode == "low-fpr":
|
62 |
+
self.threshold = BINOCULARS_FPR_THRESHOLD
|
63 |
+
elif mode == "accuracy":
|
64 |
+
self.threshold = BINOCULARS_ACCURACY_THRESHOLD
|
65 |
+
else:
|
66 |
+
raise ValueError(f"Invalid mode: {mode}")
|
67 |
+
|
68 |
+
def free_memory(self) -> None:
|
69 |
+
self.observer_model = self.observer_model.to('cpu')
|
70 |
+
self.performer_model = self.performer_model.to('cpu')
|
71 |
+
|
72 |
+
if torch.cuda.is_available():
|
73 |
+
torch.cuda.empty_cache()
|
74 |
+
torch.cuda.synchronize()
|
75 |
+
|
76 |
+
del self.observer_model
|
77 |
+
del self.performer_model
|
78 |
+
self.observer_model = None
|
79 |
+
self.performer_model = None
|
80 |
+
|
81 |
+
def _tokenize(self, batch: list[str]) -> transformers.BatchEncoding:
|
82 |
+
batch_size = len(batch)
|
83 |
+
encodings = self.tokenizer(
|
84 |
+
batch,
|
85 |
+
return_tensors="pt",
|
86 |
+
padding="longest" if batch_size > 1 else False,
|
87 |
+
truncation=True,
|
88 |
+
max_length=self.max_token_observed,
|
89 |
+
return_token_type_ids=False).to(self.observer_model.device)
|
90 |
+
return encodings
|
91 |
+
|
92 |
+
@torch.inference_mode()
|
93 |
+
def _get_logits(self, encodings: transformers.BatchEncoding) -> torch.Tensor:
|
94 |
+
observer_logits = self.observer_model(**encodings.to(DEVICE_1)).logits
|
95 |
+
performer_logits = self.performer_model(**encodings.to(DEVICE_2)).logits
|
96 |
+
if DEVICE_1 != "cpu":
|
97 |
+
torch.cuda.synchronize()
|
98 |
+
return observer_logits, performer_logits
|
99 |
+
|
100 |
+
def compute_score(self, input_text: Union[list[str], str]) -> Union[float, list[float]]:
|
101 |
+
batch = [input_text] if isinstance(input_text, str) else input_text
|
102 |
+
encodings = self._tokenize(batch)
|
103 |
+
observer_logits, performer_logits = self._get_logits(encodings)
|
104 |
+
ppl = perplexity(encodings, performer_logits)
|
105 |
+
x_ppl = entropy(observer_logits.to(DEVICE_1), performer_logits.to(DEVICE_1),
|
106 |
+
encodings.to(DEVICE_1), self.tokenizer.pad_token_id)
|
107 |
+
binoculars_scores = ppl / x_ppl
|
108 |
+
binoculars_scores = binoculars_scores.tolist()
|
109 |
+
return binoculars_scores[0] if isinstance(input_text, str) else binoculars_scores
|
110 |
+
|
111 |
+
def predict(self, input_text: Union[list[str], str]) -> Union[list[str], str]:
|
112 |
+
binoculars_scores = np.array(self.compute_score(input_text))
|
113 |
+
pred = np.where(binoculars_scores < self.threshold,
|
114 |
+
"Most likely AI-generated",
|
115 |
+
"Most likely human-generated"
|
116 |
+
).tolist()
|
117 |
+
return pred
|
binoculars/metrics.py
ADDED
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import numpy as np
|
2 |
+
import torch
|
3 |
+
import transformers
|
4 |
+
|
5 |
+
ce_loss_fn = torch.nn.CrossEntropyLoss(reduction="none")
|
6 |
+
softmax_fn = torch.nn.Softmax(dim=-1)
|
7 |
+
|
8 |
+
|
9 |
+
def perplexity(encoding: transformers.BatchEncoding,
|
10 |
+
logits: torch.Tensor,
|
11 |
+
median: bool = False,
|
12 |
+
temperature: float = 1.0):
|
13 |
+
shifted_logits = logits[..., :-1, :].contiguous() / temperature
|
14 |
+
shifted_labels = encoding.input_ids[..., 1:].contiguous()
|
15 |
+
shifted_attention_mask = encoding.attention_mask[..., 1:].contiguous()
|
16 |
+
|
17 |
+
if median:
|
18 |
+
ce_nan = (ce_loss_fn(shifted_logits.transpose(1, 2), shifted_labels).
|
19 |
+
masked_fill(~shifted_attention_mask.bool(), float("nan")))
|
20 |
+
ppl = np.nanmedian(ce_nan.cpu().float().numpy(), 1)
|
21 |
+
|
22 |
+
else:
|
23 |
+
ppl = (ce_loss_fn(shifted_logits.transpose(1, 2), shifted_labels) *
|
24 |
+
shifted_attention_mask).sum(1) / shifted_attention_mask.sum(1)
|
25 |
+
ppl = ppl.to("cpu").float().numpy()
|
26 |
+
|
27 |
+
return ppl
|
28 |
+
|
29 |
+
|
30 |
+
def entropy(p_logits: torch.Tensor,
|
31 |
+
q_logits: torch.Tensor,
|
32 |
+
encoding: transformers.BatchEncoding,
|
33 |
+
pad_token_id: int,
|
34 |
+
median: bool = False,
|
35 |
+
sample_p: bool = False,
|
36 |
+
temperature: float = 1.0):
|
37 |
+
vocab_size = p_logits.shape[-1]
|
38 |
+
total_tokens_available = q_logits.shape[-2]
|
39 |
+
p_scores, q_scores = p_logits / temperature, q_logits / temperature
|
40 |
+
|
41 |
+
p_proba = softmax_fn(p_scores).view(-1, vocab_size)
|
42 |
+
|
43 |
+
if sample_p:
|
44 |
+
p_proba = torch.multinomial(p_proba.view(-1, vocab_size), replacement=True, num_samples=1).view(-1)
|
45 |
+
|
46 |
+
q_scores = q_scores.view(-1, vocab_size)
|
47 |
+
|
48 |
+
ce = ce_loss_fn(input=q_scores, target=p_proba).view(-1, total_tokens_available)
|
49 |
+
padding_mask = (encoding.input_ids != pad_token_id).type(torch.uint8)
|
50 |
+
|
51 |
+
if median:
|
52 |
+
ce_nan = ce.masked_fill(~padding_mask.bool(), float("nan"))
|
53 |
+
agg_ce = np.nanmedian(ce_nan.cpu().float().numpy(), 1)
|
54 |
+
else:
|
55 |
+
agg_ce = (((ce * padding_mask).sum(1) / padding_mask.sum(1)).to("cpu").float().numpy())
|
56 |
+
|
57 |
+
return agg_ce
|
binoculars/utils.py
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import warnings
|
2 |
+
from transformers import AutoTokenizer
|
3 |
+
|
4 |
+
|
5 |
+
def assert_tokenizer_consistency(model_id_1, model_id_2):
|
6 |
+
identical_tokenizers = (
|
7 |
+
AutoTokenizer.from_pretrained(model_id_1).vocab
|
8 |
+
== AutoTokenizer.from_pretrained(model_id_2).vocab
|
9 |
+
)
|
10 |
+
if not identical_tokenizers:
|
11 |
+
warnings.warn(
|
12 |
+
f"Warning: Tokenizers for models '{model_id_1}' and '{model_id_2}' have different vocabularies. "
|
13 |
+
f"This may lead to inconsistent results when comparing these models. "
|
14 |
+
f"Consider using models with compatible tokenizers.",
|
15 |
+
UserWarning
|
16 |
+
)
|
binoculars_utils.py
ADDED
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from binoculars import Binoculars
|
2 |
+
|
3 |
+
def initialize_binoculars():
|
4 |
+
chat_model_pair = {
|
5 |
+
"observer": "deepseek-ai/deepseek-llm-7b-base",
|
6 |
+
"performer": "deepseek-ai/deepseek-llm-7b-chat"
|
7 |
+
}
|
8 |
+
|
9 |
+
coder_model_pair = {
|
10 |
+
"observer": "deepseek-ai/deepseek-llm-7b-base",
|
11 |
+
"performer": "deepseek-ai/deepseek-coder-7b-instruct-v1.5"
|
12 |
+
}
|
13 |
+
|
14 |
+
print("Initializing Binoculars models...")
|
15 |
+
|
16 |
+
bino_chat = Binoculars(
|
17 |
+
mode="accuracy",
|
18 |
+
observer_name_or_path=chat_model_pair["observer"],
|
19 |
+
performer_name_or_path=chat_model_pair["performer"],
|
20 |
+
max_token_observed=2048
|
21 |
+
)
|
22 |
+
|
23 |
+
bino_coder = Binoculars(
|
24 |
+
mode="accuracy",
|
25 |
+
observer_name_or_path=coder_model_pair["observer"],
|
26 |
+
performer_name_or_path=coder_model_pair["performer"],
|
27 |
+
max_token_observed=2048
|
28 |
+
)
|
29 |
+
|
30 |
+
return bino_chat, bino_coder
|
31 |
+
|
32 |
+
def compute_scores(text, bino_chat=None, bino_coder=None):
|
33 |
+
scores = {}
|
34 |
+
|
35 |
+
if bino_chat:
|
36 |
+
#print("Computing score_chat...")
|
37 |
+
scores['score_chat'] = bino_chat.compute_score(text)
|
38 |
+
|
39 |
+
if bino_coder:
|
40 |
+
#print("Computing score_coder...")
|
41 |
+
scores['score_coder'] = bino_coder.compute_score(text)
|
42 |
+
|
43 |
+
return scores
|
demo/binary_classifier_demo.py
ADDED
@@ -0,0 +1,191 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
__all__ = ["binary_app"]
|
2 |
+
|
3 |
+
import gradio as gr
|
4 |
+
import torch
|
5 |
+
import os
|
6 |
+
|
7 |
+
from model_utils import load_model, classify_text
|
8 |
+
from binoculars_utils import initialize_binoculars, compute_scores
|
9 |
+
|
10 |
+
# Initialize Binoculars models
|
11 |
+
bino_chat, bino_coder = initialize_binoculars()
|
12 |
+
|
13 |
+
# Load binary classifier model
|
14 |
+
model, scaler, label_encoder, imputer = load_model()
|
15 |
+
|
16 |
+
DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
17 |
+
MINIMUM_TOKENS = 50
|
18 |
+
|
19 |
+
SAMPLE_TEXT = """Привет! Я хотел бы рассказать вам о своём опыте путешествия по Петербургу. Невероятный город с богатой историей и красивой архитектурой. Особенно запомнился Эрмитаж с его огромной коллекцией произведений искусства. Также понравилась прогулка по каналам города, где можно увидеть множество старинных мостов и зданий."""
|
20 |
+
|
21 |
+
css = """
|
22 |
+
.human-text {
|
23 |
+
color: black !important;
|
24 |
+
line-height: 1.9em;
|
25 |
+
padding: 0.5em;
|
26 |
+
background: #ccffcc;
|
27 |
+
border-radius: 0.5rem;
|
28 |
+
font-weight: bold;
|
29 |
+
}
|
30 |
+
.ai-text {
|
31 |
+
color: black !important;
|
32 |
+
line-height: 1.9em;
|
33 |
+
padding: 0.5em;
|
34 |
+
background: #ffad99;
|
35 |
+
border-radius: 0.5rem;
|
36 |
+
font-weight: bold;
|
37 |
+
}
|
38 |
+
.analysis-block {
|
39 |
+
background: #f5f5f5;
|
40 |
+
padding: 15px;
|
41 |
+
border-radius: 8px;
|
42 |
+
margin-top: 10px;
|
43 |
+
}
|
44 |
+
.scores {
|
45 |
+
font-size: 1.1em;
|
46 |
+
padding: 10px;
|
47 |
+
background: #e6f7ff;
|
48 |
+
border-radius: 5px;
|
49 |
+
margin: 10px 0;
|
50 |
+
}
|
51 |
+
"""
|
52 |
+
|
53 |
+
def run_binary_classifier(text, show_analysis=False):
|
54 |
+
if len(text.strip()) < MINIMUM_TOKENS:
|
55 |
+
return gr.Markdown(f"Текст слишком короткий. Требуется минимум {MINIMUM_TOKENS} символов."), None, None
|
56 |
+
|
57 |
+
# Compute scores using binoculars
|
58 |
+
scores = compute_scores(text, bino_chat, bino_coder)
|
59 |
+
|
60 |
+
# Run classification
|
61 |
+
result = classify_text(text, model, scaler, label_encoder, imputer=imputer, scores=scores)
|
62 |
+
|
63 |
+
# Format results
|
64 |
+
predicted_class = result['predicted_class']
|
65 |
+
probabilities = result['probabilities']
|
66 |
+
|
67 |
+
# Format probabilities
|
68 |
+
prob_str = ""
|
69 |
+
for cls, prob in probabilities.items():
|
70 |
+
prob_str += f"- {cls}: {prob:.4f}\n"
|
71 |
+
|
72 |
+
# Format scores
|
73 |
+
scores_str = ""
|
74 |
+
if scores:
|
75 |
+
scores_str = "### Binoculars Scores\n"
|
76 |
+
if 'score_chat' in scores:
|
77 |
+
scores_str += f"- Score Chat: {scores['score_chat']:.4f}\n"
|
78 |
+
if 'score_coder' in scores:
|
79 |
+
scores_str += f"- Score Coder: {scores['score_coder']:.4f}\n"
|
80 |
+
|
81 |
+
# Result markdown
|
82 |
+
class_style = "human-text" if predicted_class == "Human" else "ai-text"
|
83 |
+
result_md = f"""
|
84 |
+
## Результат классификации
|
85 |
+
|
86 |
+
Предсказанный класс: <span class="{class_style}">{predicted_class}</span>
|
87 |
+
|
88 |
+
### Вероятности классов:
|
89 |
+
{prob_str}
|
90 |
+
|
91 |
+
{scores_str}
|
92 |
+
"""
|
93 |
+
|
94 |
+
# Analysis markdown
|
95 |
+
analysis_md = None
|
96 |
+
if show_analysis:
|
97 |
+
features = result['features']
|
98 |
+
text_analysis = result['text_analysis']
|
99 |
+
|
100 |
+
analysis_md = "## Анализ текста\n\n"
|
101 |
+
|
102 |
+
# Basic statistics
|
103 |
+
analysis_md += "### Основная статистика\n"
|
104 |
+
analysis_md += f"- Всего токенов: {text_analysis['basic_stats']['total_tokens']}\n"
|
105 |
+
analysis_md += f"- Всего слов: {text_analysis['basic_stats']['total_words']}\n"
|
106 |
+
analysis_md += f"- Уникальных слов: {text_analysis['basic_stats']['unique_words']}\n"
|
107 |
+
analysis_md += f"- Стоп-слов: {text_analysis['basic_stats']['stop_words']}\n"
|
108 |
+
analysis_md += f"- Средняя длина слова: {text_analysis['basic_stats']['avg_word_length']:.2f} символов\n\n"
|
109 |
+
|
110 |
+
# Lexical diversity
|
111 |
+
analysis_md += "### Лексическое разнообразие\n"
|
112 |
+
analysis_md += f"- TTR (Type-Token Ratio): {text_analysis['lexical_diversity']['ttr']:.3f}\n"
|
113 |
+
analysis_md += f"- MTLD (упрощенный): {text_analysis['lexical_diversity']['mtld']:.2f}\n\n"
|
114 |
+
|
115 |
+
# Text structure
|
116 |
+
analysis_md += "### Структура текста\n"
|
117 |
+
analysis_md += f"- Количество предложений: {text_analysis['text_structure']['sentence_count']}\n"
|
118 |
+
analysis_md += f"- Средняя длина предложения: {text_analysis['text_structure']['avg_sentence_length']:.2f} токенов\n\n"
|
119 |
+
|
120 |
+
# Readability
|
121 |
+
analysis_md += "### Читабельность\n"
|
122 |
+
analysis_md += f"- Flesch-Kincaid score: {text_analysis['readability']['flesh_kincaid_score']:.2f}\n"
|
123 |
+
analysis_md += f"- Процент дл��нных слов: {text_analysis['readability']['long_words_percent']:.2f}%\n\n"
|
124 |
+
|
125 |
+
# Semantic coherence
|
126 |
+
analysis_md += "### Семантическая связность\n"
|
127 |
+
analysis_md += f"- Средняя связность между предложениями: {text_analysis['semantic_coherence']['avg_coherence_score']:.3f}\n"
|
128 |
+
|
129 |
+
return gr.Markdown(result_md), gr.Markdown(analysis_md) if analysis_md else None, text
|
130 |
+
|
131 |
+
def reset_outputs():
|
132 |
+
return None, None, ""
|
133 |
+
|
134 |
+
with gr.Blocks(css=css, theme=gr.themes.Base()) as binary_app:
|
135 |
+
with gr.Row():
|
136 |
+
with gr.Column(scale=3):
|
137 |
+
gr.HTML("<h1>Binary Classifier: Human vs AI Text Detection</h1>")
|
138 |
+
gr.HTML("<p>This demo uses a neural network (Medium_Binary_Network) to classify text as either written by a human or generated by AI.</p>")
|
139 |
+
|
140 |
+
with gr.Row():
|
141 |
+
with gr.Column():
|
142 |
+
input_text = gr.Textbox(value=SAMPLE_TEXT, placeholder="Введите текст для анализа",
|
143 |
+
lines=10, label="Текст для анализа")
|
144 |
+
|
145 |
+
with gr.Row():
|
146 |
+
analysis_checkbox = gr.Checkbox(label="Показать детальный анализ текста", value=False)
|
147 |
+
submit_button = gr.Button("Классифицировать", variant="primary")
|
148 |
+
clear_button = gr.Button("Очистить")
|
149 |
+
|
150 |
+
with gr.Row():
|
151 |
+
with gr.Column():
|
152 |
+
result_output = gr.Markdown(label="Результат")
|
153 |
+
|
154 |
+
with gr.Row():
|
155 |
+
with gr.Column():
|
156 |
+
analysis_output = gr.Markdown(label="Анализ")
|
157 |
+
|
158 |
+
with gr.Accordion("О модели", open=False):
|
159 |
+
gr.Markdown("""
|
160 |
+
### О бинарном классификаторе
|
161 |
+
|
162 |
+
Эта демонстрация использует нейронную сеть Medium_Binary_Network для классификации текста как написанного человеком или сгенерированного ИИ.
|
163 |
+
|
164 |
+
#### Архитектура модели:
|
165 |
+
- Входной слой: Количество признаков (зависит от анализа текста)
|
166 |
+
- Скрытые слои: [256, 192, 128, 64]
|
167 |
+
- Выходной слой: 2 класса (Human, AI)
|
168 |
+
- Dropout: 0.3
|
169 |
+
|
170 |
+
#### Особенности:
|
171 |
+
- Используется анализ текста и оценки качества текста с помощью Binoculars
|
172 |
+
- Анализируются морфологические, синтаксические и семантические особенности текста
|
173 |
+
- Вычисляются показатели лексического разнообразия и читабельности
|
174 |
+
|
175 |
+
#### Рекомендации:
|
176 |
+
- Для более точной классификации рекомендуется использовать тексты длиннее 100 слов
|
177 |
+
- Модель обучена на русскоязычных текстах
|
178 |
+
""")
|
179 |
+
|
180 |
+
# Set up event handlers
|
181 |
+
submit_button.click(
|
182 |
+
fn=run_binary_classifier,
|
183 |
+
inputs=[input_text, analysis_checkbox],
|
184 |
+
outputs=[result_output, analysis_output, input_text]
|
185 |
+
)
|
186 |
+
|
187 |
+
clear_button.click(
|
188 |
+
fn=reset_outputs,
|
189 |
+
inputs=[],
|
190 |
+
outputs=[result_output, analysis_output, input_text]
|
191 |
+
)
|
ex_text.txt
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
**Национальный переходный совет Ливии обращается к Западу с просьбой ликвидировать Каддафи**\n\nБенгази, Ливия, 14 марта — Национальный переходный совет Ливии, расположенный в Бенгази, обратился к западным державам с настоятельной просьбой о принятии мер по устранению полковника Муаммара Каддафи, который в настоящее время контролирует западную часть страны. Представитель Совета Мустафа Гериани заявил, что делегация Совета официально передала запрос на проведение тактических ударов против диктатора и установление запретной зоны для полетов в понедельник, 14 марта.\n\n\"Мы готовы сделать все необходимое для разрушения режима Каддафи. Его время пришло, и никто не будет горевать о его смерти,\" — заявил Гериани на пресс-конференции, подчеркнув решимость переходного совета добиться смены власти мирным путем через международное сообщество.\n\nПока не поступило официальных заявлений от Франции и США в ответ на запрос Ливийского совета. Хотя западные страны, включая Париж и Лондон, активно стремятся создать запретную зону над Ливией, консенсус среди членов \"Большой восьмерки\" и Совета Безопасности ООН по данному вопросу еще не достигнут. Специалисты отмечают, что отсутствие единодушия среди международных лидеров может замедлить принятие необходимых мер для стабилизации ситуации в стране.\n\nКонфликт между правительственными силами из Триполи и повстанцами из Бенгази продолжает приносить значительные человеческие потери. По оценкам, число погибших колеблется от нескольких сотен до нескольких тысяч человек, что подрывает возможности для мирного разрешения конфликта. Война уже привела к серьезным разрушениям инфраструктуры и гуманитарному кризису, усугубляя страдания мирного населения.\n\nМеждународные организации и правозащитные группы выражают обеспокоенность нестабильностью в Ливии и призывают к скорейшему урегулированию конфликта. Некоторые аналитики считают, что вмешательство Запада является необходимым шагом для предотвращения дальнейшей эскалации насилия и установления демократического порядка в стране.\n\nНа данный момент остается неизвестным, какие конкретные шаги предпримут Франция, США и другие западные державы в ответ на призыв Ливийского переходного совета. Мир с напряжением ожидает дальнейших заявлений и действий международного сообщества в разрешении кризиса, который уже серьезно повлиял на будущее Ливии и стабильность региона в целом.
|
2 |
+
Национальный переходный совет Ливии в Бенгази обратился к странам Запада с просьбой ликвидировать полковника Муаммара Каддафи, контролирующего запад страны, пишет газета The Guardian. Представитель Совета Мустафа Гериани заявил, что просьбу об уничтожении диктатора Франции и США должна была передать делегация из Бенгази в понедельник 14 марта. \"Мы пояснили западным странам, что хотим ��становления над Ливией зоны, запретной для полетов, тактических ударов по боевой технике Каддафи и уничтожения его резиденции\", - сказал Гериани. На уточняющий вопрос, хочет ли он смерти диктатора, представитель Совета ответил: \"Почему бы и нет? Если он умрет, никто и слезы не уронит\". Ни Франция, ни США пока высказывания Гериани не прокомментировали. Известно, что Париж и Лондон добиваются от партнеров по \"Большой восьмерке\" (G8) и Совету Безопасности ООН установления над Ливией зоны, закрытой для полетов. Однако пока в обоих объединениях консенсуса по этому поводу нет. Ранее сам Муаммар Каддафи назвал участников Совета в Бенгази \"предателями и шпионами\" и пообещал награду за голову их руководителя - Мустафы Абдель Джалиля. Противостояние между Триполи и Бенгази продолжается уже месяц. За это время в ходе боев и беспорядков по всей Ливии погибли от нескольких сотен до нескольких тысяч человек.
|
3 |
+
Национальный переходный совет Ливии, базирующийся в Бенгази, обратился к западным державам с требованием устранить полковника Муаммара Каддафи, который контролирует западную часть страны, сообщает газета The Guardian. Мустафа Гериани, представитель Совета, уточнил, что запрос на ликвидацию диктатора должен был быть передан франко-американской делегации из Бенгази в понедельник, 14 марта. «Мы объяснили западным государствам наше намерение создать над Ливией воздушную запретную зону, провести тактические удары по военной технике Каддафи и разрушить его резиденцию», — заявил Гериани. На вопрос о том, желает ли он смерти диктатора, представитель Совета ответил: «Почему бы и нет? Если он погибнет, никто не оплачет его». На данный момент Франция и США не прокомментировали заявления Гериани. Известно, что власти Парижа и Лондона прилагают усилия к партнерам по «Большой восьмерке» (G8) и Совету Безопасности ООН для создания воздушной запретной зоны над Ливией. Однако в этих организациях пока не достигнут общий консенсус по этому вопросу. Ранее Муаммар Каддафи назвал членов Совета в Бенгази «предателями и шпионами» и пообещал вознаграждение за голову их лидера — Мустафы Абдел Джалиля. Конфликт между Триполи и Бенгази продолжается уже месяц, в ходе которого из-за боевых действий и беспорядков по всей Ливии погибло от сотен до нескольких тысяч человек.
|
feature_extraction.py
ADDED
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import pandas as pd
|
2 |
+
from text_analysis import analyze_text
|
3 |
+
|
4 |
+
def extract_features(text, feature_config=None, scores=None):
|
5 |
+
if feature_config is None:
|
6 |
+
feature_config = {
|
7 |
+
'basic_scores': True,
|
8 |
+
'basic_text_stats': ['total_tokens', 'total_words', 'unique_words', 'stop_words', 'avg_word_length'],
|
9 |
+
'morphological': ['pos_distribution', 'unique_lemmas', 'lemma_word_ratio'],
|
10 |
+
'syntactic': ['dependencies', 'noun_chunks'],
|
11 |
+
'entities': ['total_entities', 'entity_types'],
|
12 |
+
'diversity': ['ttr', 'mtld'],
|
13 |
+
'structure': ['sentence_count', 'avg_sentence_length', 'question_sentences', 'exclamation_sentences'],
|
14 |
+
'readability': ['words_per_sentence', 'syllables_per_word', 'flesh_kincaid_score', 'long_words_percent'],
|
15 |
+
'semantic': True
|
16 |
+
}
|
17 |
+
|
18 |
+
text_analysis = analyze_text(text)
|
19 |
+
|
20 |
+
features_df = pd.DataFrame(index=[0])
|
21 |
+
|
22 |
+
if scores:
|
23 |
+
features_df['score_chat'] = scores.get('score_chat', 0)
|
24 |
+
features_df['score_coder'] = scores.get('score_coder', 0)
|
25 |
+
else:
|
26 |
+
features_df['score_chat'] = 0
|
27 |
+
features_df['score_coder'] = 0
|
28 |
+
print("Warning: No scores provided, using zeros for score_chat and score_coder")
|
29 |
+
|
30 |
+
if feature_config.get('basic_text_stats'):
|
31 |
+
for feature in feature_config['basic_text_stats']:
|
32 |
+
features_df[f'basic_{feature}'] = text_analysis.get('basic_stats', {}).get(feature, 0)
|
33 |
+
|
34 |
+
if feature_config.get('morphological'):
|
35 |
+
for feature in feature_config['morphological']:
|
36 |
+
if feature == 'pos_distribution':
|
37 |
+
pos_types = ['NOUN', 'VERB', 'ADJ', 'ADV', 'PROPN', 'DET', 'ADP', 'PRON', 'CCONJ', 'SCONJ']
|
38 |
+
for pos in pos_types:
|
39 |
+
features_df[f'pos_{pos}'] = text_analysis.get('morphological_analysis', {}).get('pos_distribution', {}).get(pos, 0)
|
40 |
+
else:
|
41 |
+
features_df[f'morph_{feature}'] = text_analysis.get('morphological_analysis', {}).get(feature, 0)
|
42 |
+
|
43 |
+
if feature_config.get('syntactic'):
|
44 |
+
for feature in feature_config['syntactic']:
|
45 |
+
if feature == 'dependencies':
|
46 |
+
dep_types = ['nsubj', 'obj', 'amod', 'nmod', 'ROOT', 'punct', 'case']
|
47 |
+
for dep in dep_types:
|
48 |
+
features_df[f'dep_{dep}'] = text_analysis.get('syntactic_analysis', {}).get('dependencies', {}).get(dep, 0)
|
49 |
+
else:
|
50 |
+
features_df[f'synt_{feature}'] = text_analysis.get('syntactic_analysis', {}).get(feature, 0)
|
51 |
+
|
52 |
+
if feature_config.get('entities'):
|
53 |
+
for feature in feature_config['entities']:
|
54 |
+
if feature == 'entity_types':
|
55 |
+
entity_types = ['PER', 'LOC', 'ORG']
|
56 |
+
for ent in entity_types:
|
57 |
+
features_df[f'ent_{ent}'] = text_analysis.get('named_entities', {}).get('entity_types', {}).get(ent, 0)
|
58 |
+
else:
|
59 |
+
features_df[f'ent_{feature}'] = text_analysis.get('named_entities', {}).get(feature, 0)
|
60 |
+
|
61 |
+
if feature_config.get('diversity'):
|
62 |
+
for feature in feature_config['diversity']:
|
63 |
+
features_df[f'div_{feature}'] = text_analysis.get('lexical_diversity', {}).get(feature, 0)
|
64 |
+
|
65 |
+
if feature_config.get('structure'):
|
66 |
+
for feature in feature_config['structure']:
|
67 |
+
features_df[f'struct_{feature}'] = text_analysis.get('text_structure', {}).get(feature, 0)
|
68 |
+
|
69 |
+
if feature_config.get('readability'):
|
70 |
+
for feature in feature_config['readability']:
|
71 |
+
features_df[f'read_{feature}'] = text_analysis.get('readability', {}).get(feature, 0)
|
72 |
+
|
73 |
+
if feature_config.get('semantic'):
|
74 |
+
features_df['semantic_coherence'] = text_analysis.get('semantic_coherence', {}).get('avg_coherence_score', 0)
|
75 |
+
|
76 |
+
return features_df, text_analysis
|
main.py
ADDED
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import argparse
|
2 |
+
import pandas as pd
|
3 |
+
|
4 |
+
from text_analysis import show_text_analysis
|
5 |
+
from binoculars_utils import initialize_binoculars, compute_scores
|
6 |
+
from model_utils import load_model, classify_text
|
7 |
+
|
8 |
+
def main():
|
9 |
+
parser = argparse.ArgumentParser(description='Text classifier demonstration (Human vs AI)')
|
10 |
+
parser.add_argument('--text', type=str, help='Text for classification')
|
11 |
+
parser.add_argument('--file', type=str, help='Path to file with text')
|
12 |
+
parser.add_argument('--analysis', action='store_true', help='Show detailed text analysis')
|
13 |
+
parser.add_argument('--compute-scores', action='store_true', help='Compute score_chat and score_coder')
|
14 |
+
args = parser.parse_args()
|
15 |
+
|
16 |
+
bino_chat = None
|
17 |
+
bino_coder = None
|
18 |
+
if args.compute_scores:
|
19 |
+
bino_chat, bino_coder = initialize_binoculars()
|
20 |
+
|
21 |
+
print("Loading binary classifier model...")
|
22 |
+
model, scaler, label_encoder, imputer = load_model()
|
23 |
+
|
24 |
+
if args.text:
|
25 |
+
text = args.text
|
26 |
+
elif args.file:
|
27 |
+
with open(args.file, 'r', encoding='utf-8') as f:
|
28 |
+
text = f.read()
|
29 |
+
else:
|
30 |
+
text = input("Enter text for classification: ")
|
31 |
+
|
32 |
+
scores = None
|
33 |
+
if args.compute_scores:
|
34 |
+
scores = compute_scores(text, bino_chat, bino_coder)
|
35 |
+
|
36 |
+
print(f"\nAnalyzing text...")
|
37 |
+
result = classify_text(text, model, scaler, label_encoder, imputer=imputer, scores=scores)
|
38 |
+
|
39 |
+
print("\n" + "="*50)
|
40 |
+
print("CLASSIFICATION RESULTS")
|
41 |
+
print("="*50)
|
42 |
+
print(f"Predicted class: {result['predicted_class']}")
|
43 |
+
print("Class probabilities:")
|
44 |
+
for cls, prob in result['probabilities'].items():
|
45 |
+
print(f" - {cls}: {prob:.4f}")
|
46 |
+
|
47 |
+
if scores:
|
48 |
+
print("\nComputed scores:")
|
49 |
+
if 'score_chat' in scores:
|
50 |
+
print(f" - Score Chat: {scores['score_chat']:.4f}")
|
51 |
+
if 'score_coder' in scores:
|
52 |
+
print(f" - Score Coder: {scores['score_coder']:.4f}")
|
53 |
+
|
54 |
+
if args.analysis:
|
55 |
+
show_text_analysis(result['text_analysis'])
|
56 |
+
|
57 |
+
if args.compute_scores:
|
58 |
+
if bino_chat:
|
59 |
+
bino_chat.free_memory()
|
60 |
+
if bino_coder:
|
61 |
+
bino_coder.free_memory()
|
62 |
+
|
63 |
+
if __name__ == "__main__":
|
64 |
+
main()
|
65 |
+
|
66 |
+
|
model_utils.py
ADDED
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import torch
|
3 |
+
import joblib
|
4 |
+
import numpy as np
|
5 |
+
from sklearn.impute import SimpleImputer
|
6 |
+
from NN_classifier.simple_binary_classifier import Medium_Binary_Network
|
7 |
+
from feature_extraction import extract_features
|
8 |
+
import pandas as pd
|
9 |
+
|
10 |
+
DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
11 |
+
|
12 |
+
def load_model(model_dir='models/medium_binary_classifier'):
|
13 |
+
model_path = os.path.join(model_dir, 'nn_model.pt')
|
14 |
+
scaler_path = os.path.join(model_dir, 'scaler.joblib')
|
15 |
+
encoder_path = os.path.join(model_dir, 'label_encoder.joblib')
|
16 |
+
imputer_path = os.path.join(model_dir, 'imputer.joblib')
|
17 |
+
|
18 |
+
if not os.path.exists(model_path):
|
19 |
+
raise FileNotFoundError(f"Model not found at: {model_path}")
|
20 |
+
|
21 |
+
label_encoder = joblib.load(encoder_path)
|
22 |
+
scaler = joblib.load(scaler_path)
|
23 |
+
|
24 |
+
imputer = None
|
25 |
+
if os.path.exists(imputer_path):
|
26 |
+
imputer = joblib.load(imputer_path)
|
27 |
+
else:
|
28 |
+
print("Warning: Imputer not found, will create a new one during classification")
|
29 |
+
|
30 |
+
input_size = scaler.n_features_in_
|
31 |
+
|
32 |
+
model = Medium_Binary_Network(input_size, hidden_sizes=[256, 192, 128, 64], dropout=0.3).to(DEVICE)
|
33 |
+
model.load_state_dict(torch.load(model_path, map_location=DEVICE))
|
34 |
+
model.eval()
|
35 |
+
|
36 |
+
if imputer is not None:
|
37 |
+
try:
|
38 |
+
if hasattr(imputer, 'feature_names_in_'):
|
39 |
+
print(f"Imputer has {len(imputer.feature_names_in_)} features")
|
40 |
+
print(f"First few feature names: {imputer.feature_names_in_[:5]}")
|
41 |
+
else:
|
42 |
+
print("Warning: Imputer does not have feature_names_in_ attribute")
|
43 |
+
except Exception as e:
|
44 |
+
print(f"Error checking imputer: {str(e)}")
|
45 |
+
|
46 |
+
return model, scaler, label_encoder, imputer
|
47 |
+
|
48 |
+
def classify_text(text, model, scaler, label_encoder, imputer=None, scores=None):
|
49 |
+
features_df, text_analysis = extract_features(text, scores=scores)
|
50 |
+
|
51 |
+
if imputer is not None:
|
52 |
+
expected_feature_names = imputer.feature_names_in_
|
53 |
+
else:
|
54 |
+
expected_feature_names = None
|
55 |
+
|
56 |
+
if expected_feature_names is not None:
|
57 |
+
aligned_features = pd.DataFrame(columns=expected_feature_names)
|
58 |
+
|
59 |
+
for col in features_df.columns:
|
60 |
+
if col in expected_feature_names:
|
61 |
+
aligned_features[col] = features_df[col]
|
62 |
+
|
63 |
+
for col in expected_feature_names:
|
64 |
+
if col not in aligned_features.columns or aligned_features[col].isnull().all():
|
65 |
+
aligned_features[col] = 0
|
66 |
+
print(f"Added missing feature: {col}")
|
67 |
+
|
68 |
+
features_df = aligned_features
|
69 |
+
|
70 |
+
if imputer is None:
|
71 |
+
print("Warning: No imputer provided, creating a new one")
|
72 |
+
imputer = SimpleImputer(strategy='mean')
|
73 |
+
features = imputer.fit_transform(features_df)
|
74 |
+
else:
|
75 |
+
features = imputer.transform(features_df)
|
76 |
+
|
77 |
+
features_scaled = scaler.transform(features)
|
78 |
+
|
79 |
+
features_tensor = torch.FloatTensor(features_scaled).to(DEVICE)
|
80 |
+
|
81 |
+
with torch.no_grad():
|
82 |
+
outputs = model(features_tensor)
|
83 |
+
probabilities = torch.softmax(outputs, dim=1)
|
84 |
+
pred_class = torch.argmax(probabilities, dim=1).item()
|
85 |
+
|
86 |
+
predicted_label = label_encoder.classes_[pred_class]
|
87 |
+
|
88 |
+
probs_dict = {label_encoder.classes_[i]: probabilities[0][i].item() for i in range(len(label_encoder.classes_))}
|
89 |
+
|
90 |
+
return {
|
91 |
+
'predicted_class': predicted_label,
|
92 |
+
'probabilities': probs_dict,
|
93 |
+
'features': features_df,
|
94 |
+
'text_analysis': text_analysis,
|
95 |
+
'scores': scores
|
96 |
+
}
|
models/medium_binary_classifier/cv_results.json
ADDED
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"fold_metrics": [
|
3 |
+
{
|
4 |
+
"fold": 1,
|
5 |
+
"accuracy": 0.8263888888888888,
|
6 |
+
"precision": 0.8280646002798397,
|
7 |
+
"recall": 0.8263888888888888,
|
8 |
+
"f1": 0.8271100936623971,
|
9 |
+
"val_loss": 0.3269847333431244
|
10 |
+
},
|
11 |
+
{
|
12 |
+
"fold": 2,
|
13 |
+
"accuracy": 0.8194444444444444,
|
14 |
+
"precision": 0.8261029411764705,
|
15 |
+
"recall": 0.8194444444444444,
|
16 |
+
"f1": 0.8216165413533835,
|
17 |
+
"val_loss": 0.35324224829673767
|
18 |
+
},
|
19 |
+
{
|
20 |
+
"fold": 3,
|
21 |
+
"accuracy": 0.8331402085747392,
|
22 |
+
"precision": 0.8434186476644773,
|
23 |
+
"recall": 0.8331402085747392,
|
24 |
+
"f1": 0.8358602090830395,
|
25 |
+
"val_loss": 0.306135892868042
|
26 |
+
},
|
27 |
+
{
|
28 |
+
"fold": 4,
|
29 |
+
"accuracy": 0.8366164542294322,
|
30 |
+
"precision": 0.8442133250450394,
|
31 |
+
"recall": 0.8366164542294322,
|
32 |
+
"f1": 0.8388315597059784,
|
33 |
+
"val_loss": 0.3356165289878845
|
34 |
+
},
|
35 |
+
{
|
36 |
+
"fold": 5,
|
37 |
+
"accuracy": 0.8296639629200464,
|
38 |
+
"precision": 0.8434535764325679,
|
39 |
+
"recall": 0.8296639629200464,
|
40 |
+
"f1": 0.8328916049247007,
|
41 |
+
"val_loss": 0.3397574722766876
|
42 |
+
}
|
43 |
+
],
|
44 |
+
"overall": {
|
45 |
+
"accuracy": 0.8290479499652537,
|
46 |
+
"precision": 0.8366643662464281,
|
47 |
+
"recall": 0.8290479499652537,
|
48 |
+
"f1": 0.8313408894235843
|
49 |
+
},
|
50 |
+
"cross_validation": {
|
51 |
+
"mean_accuracy": 0.8290507918115102,
|
52 |
+
"std_accuracy": 0.005894169882178006,
|
53 |
+
"confidence_interval_95": [
|
54 |
+
0.8238843241167373,
|
55 |
+
0.8342172595062831
|
56 |
+
]
|
57 |
+
},
|
58 |
+
"best_fold": {
|
59 |
+
"fold": 4,
|
60 |
+
"accuracy": 0.8366164542294322
|
61 |
+
},
|
62 |
+
"model_config": {
|
63 |
+
"hidden_sizes": [
|
64 |
+
256,
|
65 |
+
192,
|
66 |
+
128,
|
67 |
+
64
|
68 |
+
],
|
69 |
+
"dropout": 0.3
|
70 |
+
}
|
71 |
+
}
|
models/medium_binary_classifier/imputer.joblib
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:188d4008a04267264ab8575a77248bc14c9918ead0e586b549fb4844cb306039
|
3 |
+
size 1975
|
models/medium_binary_classifier/label_encoder.joblib
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:324b9701f37445fe8c51ef7d6207fc862c7c5656b63581322e095ad9692597fa
|
3 |
+
size 540
|
models/medium_binary_classifier/nn_model.pt
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:6c071c24bc5ac0630a046d4f75f59dbae875635983edf7981f683b863a8dd955
|
3 |
+
size 377798
|
models/medium_binary_classifier/scaler.joblib
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:3cf2ad0003a7006486036f07c4eb51cb395e03309929ff679d7642332298c30e
|
3 |
+
size 1623
|
requirements.txt
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
sentencepiece
|
2 |
+
transformers
|
3 |
+
datasets
|
4 |
+
numpy
|
5 |
+
gradio
|
6 |
+
gradio_client
|
7 |
+
scikit-learn
|
8 |
+
seaborn
|
9 |
+
pandas
|
10 |
+
#flash_attn
|
setup.py
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from setuptools import setup, find_packages
|
2 |
+
|
3 |
+
setup(
|
4 |
+
name='Trinoculars',
|
5 |
+
version='1.0.0',
|
6 |
+
packages=find_packages(),
|
7 |
+
url='https://github.com/CoffeBank/Trinoculars',
|
8 |
+
license=open("LICENSE.md", "r", encoding="utf-8").read(),
|
9 |
+
author='',
|
10 |
+
author_email='',
|
11 |
+
description='An improved version of the Binoculars language model text detector for ru datasets.',
|
12 |
+
long_description=open("README.md", "r", encoding="utf-8").read(),
|
13 |
+
long_description_content_type="text/markdown",
|
14 |
+
install_requires=open("requirements.txt", "r", encoding="utf-8").read().splitlines(),
|
15 |
+
)
|