CoffeBank commited on
Commit
8db7949
·
1 Parent(s): f9979ab
app.py CHANGED
@@ -1,7 +1,6 @@
1
- import gradio as gr
2
-
3
- def greet(name):
4
- return "Hello " + name + "!!"
5
-
6
- demo = gr.Interface(fn=greet, inputs="text", outputs="text")
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
+ )