Empfloo commited on
Commit
9f3ece4
·
verified ·
1 Parent(s): 78ad582

Upload 10 files

Browse files
app.py ADDED
@@ -0,0 +1,412 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Tuple, Any, List
2
+
3
+ import numpy as np
4
+ import streamlit as st
5
+ import os
6
+ import torch
7
+ import random
8
+ from pathlib import Path
9
+ from os import PathLike
10
+
11
+ from peft import AutoPeftModelForSequenceClassification
12
+ from sqlalchemy.sql.functions import random
13
+ from transformers import AutoTokenizer, AutoConfig, TokenizersBackend, SentencePieceBackend
14
+ from typing import Dict
15
+ import requests
16
+ from bs4 import BeautifulSoup
17
+ import json
18
+ import pandas as pd
19
+
20
+ from deep_translator import (GoogleTranslator,
21
+ MyMemoryTranslator,
22
+ LingueeTranslator,
23
+ PonsTranslator,
24
+ single_detection)
25
+
26
+ os.environ["TRANSFORMERS_VERBOSITY"] = "error"
27
+ device = "cuda" if torch.cuda.is_available() else "cpu"
28
+ NUM_LABELS = 149
29
+
30
+
31
+ @st.cache_resource(show_spinner="Загрузка модели...")
32
+ def load_model(
33
+ model_name: str = "oracat/bert-paper-classifier-arxiv",
34
+ model_path: str | PathLike[str]= Path('./checkpoints/bert-paper-classifier-arxiv/checkpoints/final_model')
35
+ ) -> Tuple[Any, Any, AutoConfig]:
36
+
37
+ model = AutoPeftModelForSequenceClassification.from_pretrained(
38
+ model_path,
39
+ num_labels=149,
40
+ problem_type="multi_label_classification",
41
+ ignore_mismatched_sizes=True
42
+ )
43
+ tokenizer = AutoTokenizer.from_pretrained(model_name)
44
+ model_cfg = AutoConfig.from_pretrained(model_name)
45
+
46
+ model = model.to(device)
47
+ model.eval()
48
+ return model, tokenizer, model_cfg
49
+
50
+
51
+ @st.cache_data(show_spinner='Получение категорий с Arxiv...')
52
+ def fetch_arxiv_cat_describes(url: str = "https://arxiv.org/category_taxonomy") -> Dict[str, str]:
53
+ resp = requests.get(url)
54
+ resp.raise_for_status()
55
+ soup = BeautifulSoup(resp.text, "html5lib")
56
+
57
+ cat2descr = {}
58
+ for head in soup.select("h2.accordion-head"):
59
+
60
+ body = head.find_next_sibling("div", class_="accordion-body")
61
+ if body is None:
62
+ continue
63
+
64
+ for block in body.select("div.columns.divided"):
65
+ h4 = block.find("h4")
66
+ if h4 is None:
67
+ continue
68
+
69
+ cat_code = h4.contents[0].text.strip()
70
+ cat2descr[cat_code] = h4.contents[1].text.strip('()')
71
+
72
+ return cat2descr
73
+
74
+
75
+ @st.cache_data
76
+ def get_ids_to_cats(json_path=Path('./data/ids2cat.json')) -> Dict[int, str]:
77
+ if not json_path.exists():
78
+ raise FileNotFoundError(json_path)
79
+
80
+ with open(json_path, 'r') as f:
81
+ ids2cat = json.load(f)
82
+ ids2cat = {int(k): v for k, v in ids2cat.items()}
83
+
84
+ return ids2cat
85
+
86
+
87
+ @st.cache_data(show_spinner=False)
88
+ def translate_text(text: str, translator_name: str, source: str = "auto") -> str:
89
+ if not text or not text.strip():
90
+ return text
91
+
92
+ if translator_name == "Google Translator":
93
+ return GoogleTranslator(source=source, target='en').translate(text)
94
+
95
+ elif translator_name == "MyMemory Translator":
96
+ return MyMemoryTranslator(source=source, target='en-GB').translate(text)
97
+
98
+ raise ValueError(f"Неизвестный переводчик: {translator_name}")
99
+
100
+
101
+
102
+ @torch.inference_mode()
103
+ def predict(text: str) -> List[Tuple[int, float]]:
104
+
105
+ inputs = tokenizer(
106
+ text,
107
+ return_tensors="pt",
108
+ truncation=True,
109
+ padding=True,
110
+ max_length=model_cfg.max_position_embeddings,
111
+ )
112
+ inputs = {k: v.to(device) for k, v in inputs.items()}
113
+ outputs = model(**inputs)
114
+ probs = torch.sigmoid(outputs.logits[0]).cpu().tolist()
115
+ return [(i, p) for i, p in enumerate(probs)]
116
+
117
+
118
+ def go_to_phase1():
119
+ st.session_state.step = 1
120
+
121
+
122
+ def go_to_phase2():
123
+ if ((st.session_state.title_input is None or len(st.session_state.title_input) == 0 )
124
+ and (st.session_state.abstract_input is None or len(st.session_state.abstract_input) == 0)):
125
+ st.toast('Пустой заголовок и текст', icon="🚫")
126
+ return
127
+
128
+ st.session_state.step = 2
129
+ # Сохранение пользовательского ввода
130
+ st.session_state.saved_title = st.session_state.title_input
131
+ st.session_state.saved_abstract = st.session_state.abstract_input
132
+ st.session_state.saved_k = st.session_state.k_input
133
+ st.session_state.saved_translator = st.session_state.translator_choice
134
+
135
+
136
+ def clear_form():
137
+ st.session_state.title_input = None
138
+ st.session_state.abstract_input = None
139
+ st.session_state.k_input = 10
140
+ st.session_state.translator_choice = "Google Translator"
141
+
142
+ st.session_state.saved_title = None
143
+ st.session_state.saved_abstract = None
144
+ st.session_state.saved_k = 10
145
+ st.session_state.saved_translator = "Google Translator"
146
+
147
+
148
+ if 'saved_title' not in st.session_state:
149
+ st.session_state.saved_title = None
150
+
151
+ if 'saved_abstract' not in st.session_state:
152
+ st.session_state.saved_abstract = None
153
+
154
+ if 'saved_k' not in st.session_state:
155
+ st.session_state.saved_k = 10
156
+
157
+ if 'saved_translator' not in st.session_state:
158
+ st.session_state.saved_translator = "Google Translator"
159
+
160
+ if 'step' not in st.session_state:
161
+ st.session_state.step = 1
162
+
163
+
164
+ model, tokenizer, model_cfg = load_model()
165
+ cat2descr = fetch_arxiv_cat_describes()
166
+ ids2cat = get_ids_to_cats()
167
+
168
+ st.markdown("""
169
+ <style>
170
+ html, body, [class*="css"] {
171
+ font-family: "Nunito Sans", sans-serif;
172
+ }
173
+
174
+ h1, h2, h3, h4, h5, h6,
175
+ [data-testid="stMarkdownContainer"] h1,
176
+ [data-testid="stMarkdownContainer"] h2,
177
+ [data-testid="stMarkdownContainer"] h3 {
178
+ font-family: "Instrument Serif", serif !important;
179
+ font-weight: 400 !important;
180
+ }
181
+
182
+ code, pre, kbd, samp {
183
+ font-family: "JetBrains Mono", monospace !important;
184
+ }
185
+ </style>
186
+ """, unsafe_allow_html=True)
187
+
188
+
189
+ with st.container(border=True):
190
+ st.caption("О ПРОЕКТЕ")
191
+ st.subheader("BERT Multi-label классификатор arXiv-статей")
192
+
193
+ st.markdown(
194
+ """
195
+ Проект представляет собой систему **multi-label классификации научных статей arXiv**
196
+ по **заголовку** и **аннотации**.
197
+
198
+ Датасет был собран напрямую через **arXiv API**: статьи выгружались по категориям,
199
+ затем сопоставлялись с **официальной таксономией arXiv** и дедуплицировались
200
+ по нормализованному идентификатору. В результате был получен корпус
201
+ объёмом **более 1M+ примеров**.
202
+ """
203
+ )
204
+
205
+ st.markdown(
206
+ """
207
+ Модель предсказывает **сразу несколько тегов** для одной статьи.
208
+ Чтобы система была устойчивой к неполным данным, в обучении используются
209
+ два режима входа:
210
+
211
+ - **Только заголовок**
212
+ - **Заголовок + аннотация**
213
+
214
+ Выбор режима происходит прямо в **batch collator**, поэтому модель учится
215
+ работать и с полным описанием статьи, и с сокращённым вариантом.
216
+ """
217
+ )
218
+
219
+ st.markdown(
220
+ """
221
+ В качестве основы используется **BERT для sequence classification**,
222
+ дообучаемый с помощью **LoRA**.
223
+
224
+ Для борьбы с сильным дисбалансом классов применяется
225
+ **взвешенная binary cross-entropy**: вес каждого класса рассчитывается
226
+ по соотношению отрицательных и положительных примеров.
227
+ Итоговая модель развёрнута в **Streamlit-приложении**
228
+ для интерактивного предсказания arXiv-категорий.
229
+ """
230
+ )
231
+
232
+ st.divider()
233
+
234
+ c1, c2, c3 = st.columns(3)
235
+ c1.metric("Датасет", "1M+ статей")
236
+ c2.metric("Обучение", "~6 часов")
237
+ c3.metric("GPU", "H100")
238
+
239
+ st.caption(
240
+ "Trainable params: 999,317 · All params: 110,596,138 · Trainable%: 0.9036"
241
+ )
242
+
243
+ st.markdown("### Гиперпараметры обучения")
244
+
245
+ train_params = pd.DataFrame(
246
+ [
247
+ ("Базовая модель", "oracat/bert-paper-classifier-arxiv"),
248
+ ("Тип задачи", "Multi-label classification"),
249
+ ("Число классов", "149"),
250
+ ("Разделение датасета", "90% train / 10% test"),
251
+ ("Batch size (train)", "512"),
252
+ ("Batch size (eval)", "512"),
253
+ ("Число эпох", "10"),
254
+ ("Learning rate", "5e-4"),
255
+ ("LR scheduler", "cosine"),
256
+ ("Warmup steps", "10"),
257
+ ("Оптимизатор", "adamw_torch_fused"),
258
+ ("Weight decay", "0.001"),
259
+ ("Gradient accumulation steps", "1"),
260
+ ("Mixed precision", "bf16"),
261
+ ("LoRA target modules", "query, key, value"),
262
+ ("LoRA rank (r)", "16"),
263
+ ("LoRA alpha", "32"),
264
+ ("LoRA dropout", "0.05"),
265
+ ("use_rslora", "True"),
266
+ ("modules_to_save", "classifier"),
267
+ ("Функция потерь", "Weighted BCEWithLogitsLoss"),
268
+ ],
269
+ columns=["Гиперпараметр", "Значение"]
270
+ )
271
+
272
+ st.dataframe(train_params, use_container_width=True, hide_index=True)
273
+
274
+ if st.session_state.step == 1:
275
+ st.subheader('Часть 1: Ввод статьи')
276
+ with st.form(key='insert form', height='content'):
277
+
278
+ title = st.text_input(
279
+ label='Введите заголовок статьи',
280
+ placeholder='Заголовок статьи',
281
+ key='title_input',
282
+ value=st.session_state.saved_title
283
+ )
284
+
285
+ abstract = st.text_area(
286
+ label='Введите текст статьи',
287
+ placeholder='Текст статьи',
288
+ key='abstract_input',
289
+ value = st.session_state.saved_abstract,
290
+ height='content'
291
+ )
292
+
293
+ slider_help = 'Параметр регулирует вывод K наиболее вероятных тэгов из таксономии arxiv для статьи '
294
+ k = st.slider(
295
+ label='Введите top-K',
296
+ min_value=1,
297
+ max_value=20,
298
+ value = st.session_state.saved_k,
299
+ key='k_input',
300
+ help=slider_help
301
+ )
302
+
303
+ options = ["Google Translator", "MyMemory Translator"]
304
+ selectbox_help = ('Модель была обучена понимать только английский, поэтому если текст на другом языке'
305
+ ' или на смеси языков, то его необходимо сначала перевести.')
306
+ option = st.selectbox(
307
+ label='Выбор средства перевода',
308
+ options=options,
309
+ key='translator_choice',
310
+ help=selectbox_help
311
+ )
312
+
313
+ col1, col2, col3 = st.columns([1, 1, 5], gap="small")
314
+
315
+ with col1:
316
+ pressed_input = st.form_submit_button(
317
+ "Ввод",
318
+ width="stretch",
319
+ on_click=go_to_phase2
320
+ )
321
+
322
+ with col2:
323
+ pressed_clear = st.form_submit_button(
324
+ "Очистка",
325
+ width="stretch",
326
+ on_click=clear_form
327
+ )
328
+
329
+ elif st.session_state.step == 2:
330
+
331
+ if np.random.rand(1) < 0.5:
332
+ st.balloons()
333
+ else:
334
+ st.snow()
335
+
336
+ st.subheader('Часть 2: Результаты классификации')
337
+
338
+ title = (
339
+ st.session_state.saved_title
340
+ if st.session_state.saved_title is not None
341
+ else ''
342
+ )
343
+
344
+ abstract = (
345
+ st.session_state.saved_abstract
346
+ if st.session_state.saved_abstract is not None
347
+ else ''
348
+ )
349
+
350
+ k = (
351
+ st.session_state.saved_k
352
+ if st.session_state.saved_k is not None
353
+ else 10
354
+ )
355
+
356
+ translator_name = (
357
+ st.session_state.saved_translator
358
+ if st.session_state.saved_translator is not None
359
+ else "Google Translator"
360
+ )
361
+
362
+ if title and abstract:
363
+ input_text = title + '\n\n' + abstract
364
+ elif title:
365
+ input_text = title
366
+ else:
367
+ input_text = abstract
368
+
369
+ lang = single_detection(input_text, api_key='db46a38fbac48c2b159384593d450933')
370
+ translated_text = input_text
371
+ if lang != 'en':
372
+ with st.spinner(f"Перевод через {translator_name}..."):
373
+ try:
374
+ translated_text = translate_text(
375
+ text=input_text,
376
+ translator_name=translator_name,
377
+ source='auto'
378
+ )
379
+ except Exception as e:
380
+ st.error(f"Ошибка перевода: {e}")
381
+ st.stop()
382
+
383
+ with st.spinner("Модель анализирует текст..."):
384
+ probabilities = sorted(predict(translated_text), key=lambda x: x[1], reverse=True)
385
+ probs_k = [(ids2cat[x[0]], x[1]) for x in probabilities[:k]]
386
+
387
+ with st.container(border=True):
388
+ st.caption("Лучшие совпадения")
389
+ c1, c2 = st.columns([3, 1])
390
+
391
+ with c1:
392
+ best_cat = cat2descr[probs_k[0][0]]
393
+ st.markdown(f"# **{best_cat}**")
394
+ st.caption(f"Arxiv Tag: {probs_k[0][0]}")
395
+
396
+ with c2:
397
+ best_p = probs_k[0][1]
398
+ st.markdown(f"# _**{round(best_p * 100, 2)}%**_")
399
+
400
+ with st.container(border=True):
401
+ st.caption(f"Наиболее подходящие {k}/{NUM_LABELS} категорий")
402
+ st.markdown(
403
+ "<hr style='margin: 8px 0; border: none; border-top: 1px solid #ddd;'>",
404
+ unsafe_allow_html=True
405
+ )
406
+ rows = []
407
+ for cat, prob in probs_k:
408
+ rows.append({'Category': cat2descr[cat], 'Probability': f'{round(prob * 100, 2)}%', 'Arxiv Tag': cat})
409
+ df = pd.DataFrame(rows)
410
+ st.dataframe(df)
411
+
412
+ st.button(label='Назад', on_click=go_to_phase1, width="stretch")
checkpoints/bert-paper-classifier-arxiv/checkpoints/final_model/README.md ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ base_model: oracat/bert-paper-classifier-arxiv
3
+ library_name: peft
4
+ tags:
5
+ - base_model:adapter:oracat/bert-paper-classifier-arxiv
6
+ - lora
7
+ - transformers
8
+ ---
9
+
10
+ # Model Card for Model ID
11
+
12
+ <!-- Provide a quick summary of what the model is/does. -->
13
+
14
+
15
+
16
+ ## Model Details
17
+
18
+ ### Model Description
19
+
20
+ <!-- Provide a longer summary of what this model is. -->
21
+
22
+
23
+
24
+ - **Developed by:** [More Information Needed]
25
+ - **Funded by [optional]:** [More Information Needed]
26
+ - **Shared by [optional]:** [More Information Needed]
27
+ - **Model type:** [More Information Needed]
28
+ - **Language(s) (NLP):** [More Information Needed]
29
+ - **License:** [More Information Needed]
30
+ - **Finetuned from model [optional]:** [More Information Needed]
31
+
32
+ ### Model Sources [optional]
33
+
34
+ <!-- Provide the basic links for the model. -->
35
+
36
+ - **Repository:** [More Information Needed]
37
+ - **Paper [optional]:** [More Information Needed]
38
+ - **Demo [optional]:** [More Information Needed]
39
+
40
+ ## Uses
41
+
42
+ <!-- Address questions around how the model is intended to be used, including the foreseeable users of the model and those affected by the model. -->
43
+
44
+ ### Direct Use
45
+
46
+ <!-- This section is for the model use without fine-tuning or plugging into a larger ecosystem/app. -->
47
+
48
+ [More Information Needed]
49
+
50
+ ### Downstream Use [optional]
51
+
52
+ <!-- This section is for the model use when fine-tuned for a task, or when plugged into a larger ecosystem/app -->
53
+
54
+ [More Information Needed]
55
+
56
+ ### Out-of-Scope Use
57
+
58
+ <!-- This section addresses misuse, malicious use, and uses that the model will not work well for. -->
59
+
60
+ [More Information Needed]
61
+
62
+ ## Bias, Risks, and Limitations
63
+
64
+ <!-- This section is meant to convey both technical and sociotechnical limitations. -->
65
+
66
+ [More Information Needed]
67
+
68
+ ### Recommendations
69
+
70
+ <!-- This section is meant to convey recommendations with respect to the bias, risk, and technical limitations. -->
71
+
72
+ Users (both direct and downstream) should be made aware of the risks, biases and limitations of the model. More information needed for further recommendations.
73
+
74
+ ## How to Get Started with the Model
75
+
76
+ Use the code below to get started with the model.
77
+
78
+ [More Information Needed]
79
+
80
+ ## Training Details
81
+
82
+ ### Training Data
83
+
84
+ <!-- This should link to a Dataset Card, perhaps with a short stub of information on what the training data is all about as well as documentation related to data pre-processing or additional filtering. -->
85
+
86
+ [More Information Needed]
87
+
88
+ ### Training Procedure
89
+
90
+ <!-- This relates heavily to the Technical Specifications. Content here should link to that section when it is relevant to the training procedure. -->
91
+
92
+ #### Preprocessing [optional]
93
+
94
+ [More Information Needed]
95
+
96
+
97
+ #### Training Hyperparameters
98
+
99
+ - **Training regime:** [More Information Needed] <!--fp32, fp16 mixed precision, bf16 mixed precision, bf16 non-mixed precision, fp16 non-mixed precision, fp8 mixed precision -->
100
+
101
+ #### Speeds, Sizes, Times [optional]
102
+
103
+ <!-- This section provides information about throughput, start/end time, checkpoint size if relevant, etc. -->
104
+
105
+ [More Information Needed]
106
+
107
+ ## Evaluation
108
+
109
+ <!-- This section describes the evaluation protocols and provides the results. -->
110
+
111
+ ### Testing Data, Factors & Metrics
112
+
113
+ #### Testing Data
114
+
115
+ <!-- This should link to a Dataset Card if possible. -->
116
+
117
+ [More Information Needed]
118
+
119
+ #### Factors
120
+
121
+ <!-- These are the things the evaluation is disaggregating by, e.g., subpopulations or domains. -->
122
+
123
+ [More Information Needed]
124
+
125
+ #### Metrics
126
+
127
+ <!-- These are the evaluation metrics being used, ideally with a description of why. -->
128
+
129
+ [More Information Needed]
130
+
131
+ ### Results
132
+
133
+ [More Information Needed]
134
+
135
+ #### Summary
136
+
137
+
138
+
139
+ ## Model Examination [optional]
140
+
141
+ <!-- Relevant interpretability work for the model goes here -->
142
+
143
+ [More Information Needed]
144
+
145
+ ## Environmental Impact
146
+
147
+ <!-- Total emissions (in grams of CO2eq) and additional considerations, such as electricity usage, go here. Edit the suggested text below accordingly -->
148
+
149
+ Carbon emissions can be estimated using the [Machine Learning Impact calculator](https://mlco2.github.io/impact#compute) presented in [Lacoste et al. (2019)](https://arxiv.org/abs/1910.09700).
150
+
151
+ - **Hardware Type:** [More Information Needed]
152
+ - **Hours used:** [More Information Needed]
153
+ - **Cloud Provider:** [More Information Needed]
154
+ - **Compute Region:** [More Information Needed]
155
+ - **Carbon Emitted:** [More Information Needed]
156
+
157
+ ## Technical Specifications [optional]
158
+
159
+ ### Model Architecture and Objective
160
+
161
+ [More Information Needed]
162
+
163
+ ### Compute Infrastructure
164
+
165
+ [More Information Needed]
166
+
167
+ #### Hardware
168
+
169
+ [More Information Needed]
170
+
171
+ #### Software
172
+
173
+ [More Information Needed]
174
+
175
+ ## Citation [optional]
176
+
177
+ <!-- If there is a paper or blog post introducing the model, the APA and Bibtex information for that should go in this section. -->
178
+
179
+ **BibTeX:**
180
+
181
+ [More Information Needed]
182
+
183
+ **APA:**
184
+
185
+ [More Information Needed]
186
+
187
+ ## Glossary [optional]
188
+
189
+ <!-- If relevant, include terms and calculations in this section that can help readers understand the model or model card. -->
190
+
191
+ [More Information Needed]
192
+
193
+ ## More Information [optional]
194
+
195
+ [More Information Needed]
196
+
197
+ ## Model Card Authors [optional]
198
+
199
+ [More Information Needed]
200
+
201
+ ## Model Card Contact
202
+
203
+ [More Information Needed]
204
+ ### Framework versions
205
+
206
+ - PEFT 0.18.1
checkpoints/bert-paper-classifier-arxiv/checkpoints/final_model/adapter_config.json ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "alora_invocation_tokens": null,
3
+ "alpha_pattern": {},
4
+ "arrow_config": null,
5
+ "auto_mapping": null,
6
+ "base_model_name_or_path": "oracat/bert-paper-classifier-arxiv",
7
+ "bias": "none",
8
+ "corda_config": null,
9
+ "ensure_weight_tying": false,
10
+ "eva_config": null,
11
+ "exclude_modules": null,
12
+ "fan_in_fan_out": false,
13
+ "inference_mode": true,
14
+ "init_lora_weights": true,
15
+ "layer_replication": null,
16
+ "layers_pattern": null,
17
+ "layers_to_transform": null,
18
+ "loftq_config": {},
19
+ "lora_alpha": 32,
20
+ "lora_bias": false,
21
+ "lora_dropout": 0.05,
22
+ "megatron_config": null,
23
+ "megatron_core": "megatron.core",
24
+ "modules_to_save": [
25
+ "classifier",
26
+ "classifier",
27
+ "score"
28
+ ],
29
+ "peft_type": "LORA",
30
+ "peft_version": "0.18.1",
31
+ "qalora_group_size": 16,
32
+ "r": 16,
33
+ "rank_pattern": {},
34
+ "revision": null,
35
+ "target_modules": [
36
+ "query",
37
+ "value",
38
+ "key"
39
+ ],
40
+ "target_parameters": null,
41
+ "task_type": "SEQ_CLS",
42
+ "trainable_token_indices": null,
43
+ "use_dora": false,
44
+ "use_qalora": false,
45
+ "use_rslora": true
46
+ }
checkpoints/bert-paper-classifier-arxiv/checkpoints/final_model/adapter_model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:757a3ac2f9449e70b06049217c5787290009a89a51d88c92956c8997f8e54532
3
+ size 4007580
data/cat2ids.json ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "astro-ph.CO": 0,
3
+ "astro-ph.EP": 1,
4
+ "astro-ph.GA": 2,
5
+ "astro-ph.HE": 3,
6
+ "astro-ph.IM": 4,
7
+ "astro-ph.SR": 5,
8
+ "cond-mat.dis-nn": 6,
9
+ "cond-mat.mes-hall": 7,
10
+ "cond-mat.mtrl-sci": 8,
11
+ "cond-mat.other": 9,
12
+ "cond-mat.quant-gas": 10,
13
+ "cond-mat.soft": 11,
14
+ "cond-mat.stat-mech": 12,
15
+ "cond-mat.str-el": 13,
16
+ "cond-mat.supr-con": 14,
17
+ "cs.AI": 15,
18
+ "cs.AR": 16,
19
+ "cs.CC": 17,
20
+ "cs.CE": 18,
21
+ "cs.CG": 19,
22
+ "cs.CL": 20,
23
+ "cs.CR": 21,
24
+ "cs.CV": 22,
25
+ "cs.CY": 23,
26
+ "cs.DB": 24,
27
+ "cs.DC": 25,
28
+ "cs.DL": 26,
29
+ "cs.DM": 27,
30
+ "cs.DS": 28,
31
+ "cs.ET": 29,
32
+ "cs.FL": 30,
33
+ "cs.GL": 31,
34
+ "cs.GR": 32,
35
+ "cs.GT": 33,
36
+ "cs.HC": 34,
37
+ "cs.IR": 35,
38
+ "cs.IT": 36,
39
+ "cs.LG": 37,
40
+ "cs.LO": 38,
41
+ "cs.MA": 39,
42
+ "cs.MM": 40,
43
+ "cs.MS": 41,
44
+ "cs.NE": 42,
45
+ "cs.NI": 43,
46
+ "cs.OH": 44,
47
+ "cs.OS": 45,
48
+ "cs.PF": 46,
49
+ "cs.PL": 47,
50
+ "cs.RO": 48,
51
+ "cs.SC": 49,
52
+ "cs.SD": 50,
53
+ "cs.SE": 51,
54
+ "cs.SI": 52,
55
+ "econ.EM": 53,
56
+ "econ.GN": 54,
57
+ "econ.TH": 55,
58
+ "eess.AS": 56,
59
+ "eess.IV": 57,
60
+ "eess.SP": 58,
61
+ "eess.SY": 59,
62
+ "gr-qc": 60,
63
+ "hep-ex": 61,
64
+ "hep-lat": 62,
65
+ "hep-ph": 63,
66
+ "hep-th": 64,
67
+ "math-ph": 65,
68
+ "math.AC": 66,
69
+ "math.AG": 67,
70
+ "math.AP": 68,
71
+ "math.AT": 69,
72
+ "math.CA": 70,
73
+ "math.CO": 71,
74
+ "math.CT": 72,
75
+ "math.CV": 73,
76
+ "math.DG": 74,
77
+ "math.DS": 75,
78
+ "math.FA": 76,
79
+ "math.GM": 77,
80
+ "math.GN": 78,
81
+ "math.GR": 79,
82
+ "math.GT": 80,
83
+ "math.HO": 81,
84
+ "math.KT": 82,
85
+ "math.LO": 83,
86
+ "math.MG": 84,
87
+ "math.NA": 85,
88
+ "math.NT": 86,
89
+ "math.OA": 87,
90
+ "math.OC": 88,
91
+ "math.PR": 89,
92
+ "math.QA": 90,
93
+ "math.RA": 91,
94
+ "math.RT": 92,
95
+ "math.SG": 93,
96
+ "math.SP": 94,
97
+ "math.ST": 95,
98
+ "nlin.AO": 96,
99
+ "nlin.CD": 97,
100
+ "nlin.CG": 98,
101
+ "nlin.PS": 99,
102
+ "nlin.SI": 100,
103
+ "nucl-ex": 101,
104
+ "nucl-th": 102,
105
+ "physics.acc-ph": 103,
106
+ "physics.ao-ph": 104,
107
+ "physics.app-ph": 105,
108
+ "physics.atm-clus": 106,
109
+ "physics.atom-ph": 107,
110
+ "physics.bio-ph": 108,
111
+ "physics.chem-ph": 109,
112
+ "physics.class-ph": 110,
113
+ "physics.comp-ph": 111,
114
+ "physics.data-an": 112,
115
+ "physics.ed-ph": 113,
116
+ "physics.flu-dyn": 114,
117
+ "physics.gen-ph": 115,
118
+ "physics.geo-ph": 116,
119
+ "physics.hist-ph": 117,
120
+ "physics.ins-det": 118,
121
+ "physics.med-ph": 119,
122
+ "physics.optics": 120,
123
+ "physics.plasm-ph": 121,
124
+ "physics.pop-ph": 122,
125
+ "physics.soc-ph": 123,
126
+ "physics.space-ph": 124,
127
+ "q-bio.BM": 125,
128
+ "q-bio.CB": 126,
129
+ "q-bio.GN": 127,
130
+ "q-bio.MN": 128,
131
+ "q-bio.NC": 129,
132
+ "q-bio.OT": 130,
133
+ "q-bio.PE": 131,
134
+ "q-bio.QM": 132,
135
+ "q-bio.SC": 133,
136
+ "q-bio.TO": 134,
137
+ "q-fin.CP": 135,
138
+ "q-fin.GN": 136,
139
+ "q-fin.MF": 137,
140
+ "q-fin.PM": 138,
141
+ "q-fin.PR": 139,
142
+ "q-fin.RM": 140,
143
+ "q-fin.ST": 141,
144
+ "q-fin.TR": 142,
145
+ "quant-ph": 143,
146
+ "stat.AP": 144,
147
+ "stat.CO": 145,
148
+ "stat.ME": 146,
149
+ "stat.ML": 147,
150
+ "stat.OT": 148
151
+ }
data/ids2cat.json ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "0": "astro-ph.CO",
3
+ "1": "astro-ph.EP",
4
+ "2": "astro-ph.GA",
5
+ "3": "astro-ph.HE",
6
+ "4": "astro-ph.IM",
7
+ "5": "astro-ph.SR",
8
+ "6": "cond-mat.dis-nn",
9
+ "7": "cond-mat.mes-hall",
10
+ "8": "cond-mat.mtrl-sci",
11
+ "9": "cond-mat.other",
12
+ "10": "cond-mat.quant-gas",
13
+ "11": "cond-mat.soft",
14
+ "12": "cond-mat.stat-mech",
15
+ "13": "cond-mat.str-el",
16
+ "14": "cond-mat.supr-con",
17
+ "15": "cs.AI",
18
+ "16": "cs.AR",
19
+ "17": "cs.CC",
20
+ "18": "cs.CE",
21
+ "19": "cs.CG",
22
+ "20": "cs.CL",
23
+ "21": "cs.CR",
24
+ "22": "cs.CV",
25
+ "23": "cs.CY",
26
+ "24": "cs.DB",
27
+ "25": "cs.DC",
28
+ "26": "cs.DL",
29
+ "27": "cs.DM",
30
+ "28": "cs.DS",
31
+ "29": "cs.ET",
32
+ "30": "cs.FL",
33
+ "31": "cs.GL",
34
+ "32": "cs.GR",
35
+ "33": "cs.GT",
36
+ "34": "cs.HC",
37
+ "35": "cs.IR",
38
+ "36": "cs.IT",
39
+ "37": "cs.LG",
40
+ "38": "cs.LO",
41
+ "39": "cs.MA",
42
+ "40": "cs.MM",
43
+ "41": "cs.MS",
44
+ "42": "cs.NE",
45
+ "43": "cs.NI",
46
+ "44": "cs.OH",
47
+ "45": "cs.OS",
48
+ "46": "cs.PF",
49
+ "47": "cs.PL",
50
+ "48": "cs.RO",
51
+ "49": "cs.SC",
52
+ "50": "cs.SD",
53
+ "51": "cs.SE",
54
+ "52": "cs.SI",
55
+ "53": "econ.EM",
56
+ "54": "econ.GN",
57
+ "55": "econ.TH",
58
+ "56": "eess.AS",
59
+ "57": "eess.IV",
60
+ "58": "eess.SP",
61
+ "59": "eess.SY",
62
+ "60": "gr-qc",
63
+ "61": "hep-ex",
64
+ "62": "hep-lat",
65
+ "63": "hep-ph",
66
+ "64": "hep-th",
67
+ "65": "math-ph",
68
+ "66": "math.AC",
69
+ "67": "math.AG",
70
+ "68": "math.AP",
71
+ "69": "math.AT",
72
+ "70": "math.CA",
73
+ "71": "math.CO",
74
+ "72": "math.CT",
75
+ "73": "math.CV",
76
+ "74": "math.DG",
77
+ "75": "math.DS",
78
+ "76": "math.FA",
79
+ "77": "math.GM",
80
+ "78": "math.GN",
81
+ "79": "math.GR",
82
+ "80": "math.GT",
83
+ "81": "math.HO",
84
+ "82": "math.KT",
85
+ "83": "math.LO",
86
+ "84": "math.MG",
87
+ "85": "math.NA",
88
+ "86": "math.NT",
89
+ "87": "math.OA",
90
+ "88": "math.OC",
91
+ "89": "math.PR",
92
+ "90": "math.QA",
93
+ "91": "math.RA",
94
+ "92": "math.RT",
95
+ "93": "math.SG",
96
+ "94": "math.SP",
97
+ "95": "math.ST",
98
+ "96": "nlin.AO",
99
+ "97": "nlin.CD",
100
+ "98": "nlin.CG",
101
+ "99": "nlin.PS",
102
+ "100": "nlin.SI",
103
+ "101": "nucl-ex",
104
+ "102": "nucl-th",
105
+ "103": "physics.acc-ph",
106
+ "104": "physics.ao-ph",
107
+ "105": "physics.app-ph",
108
+ "106": "physics.atm-clus",
109
+ "107": "physics.atom-ph",
110
+ "108": "physics.bio-ph",
111
+ "109": "physics.chem-ph",
112
+ "110": "physics.class-ph",
113
+ "111": "physics.comp-ph",
114
+ "112": "physics.data-an",
115
+ "113": "physics.ed-ph",
116
+ "114": "physics.flu-dyn",
117
+ "115": "physics.gen-ph",
118
+ "116": "physics.geo-ph",
119
+ "117": "physics.hist-ph",
120
+ "118": "physics.ins-det",
121
+ "119": "physics.med-ph",
122
+ "120": "physics.optics",
123
+ "121": "physics.plasm-ph",
124
+ "122": "physics.pop-ph",
125
+ "123": "physics.soc-ph",
126
+ "124": "physics.space-ph",
127
+ "125": "q-bio.BM",
128
+ "126": "q-bio.CB",
129
+ "127": "q-bio.GN",
130
+ "128": "q-bio.MN",
131
+ "129": "q-bio.NC",
132
+ "130": "q-bio.OT",
133
+ "131": "q-bio.PE",
134
+ "132": "q-bio.QM",
135
+ "133": "q-bio.SC",
136
+ "134": "q-bio.TO",
137
+ "135": "q-fin.CP",
138
+ "136": "q-fin.GN",
139
+ "137": "q-fin.MF",
140
+ "138": "q-fin.PM",
141
+ "139": "q-fin.PR",
142
+ "140": "q-fin.RM",
143
+ "141": "q-fin.ST",
144
+ "142": "q-fin.TR",
145
+ "143": "quant-ph",
146
+ "144": "stat.AP",
147
+ "145": "stat.CO",
148
+ "146": "stat.ME",
149
+ "147": "stat.ML",
150
+ "148": "stat.OT"
151
+ }
download_data.py ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict
2
+ from collections import Counter
3
+ import matplotlib.pyplot as plt
4
+ from tqdm import tqdm
5
+ import json
6
+ import re
7
+ from pathlib import Path
8
+ import arxiv
9
+ import pandas as pd
10
+ import requests
11
+ from bs4 import BeautifulSoup
12
+ from tqdm.auto import tqdm
13
+
14
+
15
+ TAXONOMY_URL = "https://arxiv.org/category_taxonomy"
16
+
17
+
18
+ def fetch_arxiv_categories(url: str = "https://arxiv.org/category_taxonomy") -> Dict[str, str]:
19
+ resp = requests.get(url)
20
+ resp.raise_for_status()
21
+ soup = BeautifulSoup(resp.text, "html5lib")
22
+
23
+ cat2main = {}
24
+ for head in soup.select("h2.accordion-head"):
25
+ main_class = head.get_text(strip=True)
26
+
27
+ body = head.find_next_sibling("div", class_="accordion-body")
28
+ if body is None:
29
+ continue
30
+
31
+ for block in body.select("div.columns.divided"):
32
+ h4 = block.find("h4")
33
+ if h4 is None:
34
+ continue
35
+
36
+ cat_code = h4.contents[0].text.strip()
37
+ cat2main[cat_code] = main_class
38
+
39
+ return cat2main
40
+
41
+ def normalize_arxiv_id(arxiv_id: str) -> str:
42
+ raw_id = arxiv_id.split("/")[-1] # 2107.05580v1
43
+ raw_id = re.sub(r"v\d+$", "", raw_id) # 2107.05580
44
+ raw_id = re.sub(r"\D", "", raw_id) # 210705580
45
+ return raw_id
46
+
47
+ def make_dataset(data_json_path, remain_cats_path, categories: Dict[str, str], num_class_samples: int) -> None:
48
+
49
+ if Path(data_json_path).exists():
50
+ with open(data_json_path, "r", encoding="utf-8") as f:
51
+ records = json.load(f)
52
+ else:
53
+ records = {}
54
+
55
+ if Path(remain_cats_path).exists():
56
+ with open(remain_cats_path, "r", encoding="utf-8") as f:
57
+ remain_cats = f.readline().split()
58
+ else:
59
+ remain_cats = list(categories.keys())
60
+
61
+ client = arxiv.Client(
62
+ page_size=1000,
63
+ delay_seconds=3,
64
+ num_retries=3
65
+ )
66
+
67
+ processed_cats = []
68
+ try:
69
+ for category in tqdm(remain_cats, 'Fetching Categories'):
70
+
71
+ search = arxiv.Search(
72
+ query=f"cat:{category}",
73
+ max_results=num_class_samples,
74
+ sort_by=arxiv.SortCriterion.Relevance,
75
+ sort_order=arxiv.SortOrder.Descending
76
+ )
77
+
78
+ buffer = {}
79
+ for result in client.results(search):
80
+ if any([result.title is None,
81
+ result.categories is None,
82
+ result.entry_id is None,
83
+ result.summary is None]):
84
+ continue
85
+
86
+ arxiv_id = normalize_arxiv_id(result.entry_id)
87
+
88
+ base_cats = set()
89
+ target_cats = set()
90
+ for cat in result.categories:
91
+ if cat not in categories.keys():
92
+ continue
93
+ base_cats.add(categories[cat])
94
+ target_cats.add(cat)
95
+
96
+ if arxiv_id not in records.keys():
97
+ new_record = {
98
+ 'title': result.title,
99
+ 'abstract': result.summary,
100
+ 'categories': list(target_cats),
101
+ 'base_categories': list(base_cats),
102
+ }
103
+
104
+ buffer[arxiv_id] = new_record
105
+
106
+ records.update(buffer)
107
+ del buffer
108
+ processed_cats.append(category)
109
+
110
+ finally:
111
+ with open(data_json_path, "w", encoding="utf-8") as f:
112
+ json.dump(records, f, ensure_ascii=False, indent=4)
113
+
114
+ processed_cats_set = set(processed_cats)
115
+ new_remain_cats = [cat for cat in remain_cats if cat not in processed_cats_set]
116
+
117
+ with open(remain_cats_path, "w", encoding="utf-8") as f:
118
+ f.write(" ".join(new_remain_cats))
119
+
120
+
121
+ if __name__ == "__main__":
122
+ data_path = Path('dataset.json')
123
+ remain_cats_path = Path('./logs/remain_cats.txt')
124
+
125
+ remain_cats_path.parent.mkdir(parents=True, exist_ok=True)
126
+ categories = fetch_arxiv_categories("https://arxiv.org/category_taxonomy")
127
+
128
+ if not remain_cats_path.exists():
129
+ with open(remain_cats_path, 'w', encoding='utf-8') as file:
130
+ file.write(' '.join(categories.keys()))
131
+
132
+ #make_dataset(data_path, remain_cats_path, categories, 2)
133
+
134
+ with open(data_path, "r", encoding="utf-8") as f:
135
+ dataset = json.load(f)
136
+
137
+ dataset = pd.DataFrame.from_dict(dataset, orient='index').reset_index()
138
+
139
+ data = []
140
+ for x in dataset['categories']:
141
+ data.extend(x)
142
+
143
+ cnt = Counter(data)
144
+ print(len(cnt.keys()))
145
+
146
+ items = cnt.most_common()
147
+
148
+ labels = [x[0] for x in items]
149
+ values = [x[1] for x in items]
150
+
151
+ plt.figure(figsize=(10, max(4, len(labels) * 0.4)))
152
+ plt.barh(labels, values)
153
+ plt.xlabel("Count")
154
+ plt.ylabel("Object")
155
+ plt.title("Counter result")
156
+ plt.gca().invert_yaxis()
157
+ plt.tight_layout()
158
+ plt.show()
inference.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from peft import AutoPeftModelForSequenceClassification
2
+ from transformers import AutoTokenizer
3
+ from pathlib import Path
4
+ import torch
5
+ import json
6
+
7
+ model_name = "oracat/bert-paper-classifier-arxiv"
8
+ output_model_path = Path(f"./checkpoints/{model_name.split('/')[-1]}/checkpoints")
9
+ save_path = Path("./data")
10
+
11
+ device = "cuda" if torch.cuda.is_available() else "cpu"
12
+
13
+ # Загружаем id -> category
14
+ with open(save_path / "ids2cat.json", "r", encoding="utf-8") as f:
15
+ ids2cat = json.load(f)
16
+ ids2cat = {int(k): v for k, v in ids2cat.items()}
17
+
18
+ num_labels = len(ids2cat)
19
+
20
+ # Загружаем PEFT-модель из папки final_model
21
+ model = AutoPeftModelForSequenceClassification.from_pretrained(
22
+ output_model_path / "final_model",
23
+ num_labels=num_labels,
24
+ problem_type="multi_label_classification",
25
+ ignore_mismatched_sizes=True
26
+ )
27
+
28
+ tokenizer = AutoTokenizer.from_pretrained(model_name)
29
+
30
+ model = model.to(device)
31
+ model.eval()
32
+
33
+
34
+ @torch.inference_mode()
35
+ def predict(
36
+ text: str,
37
+ threshold: float = 0.5,
38
+ max_length: int = 512,
39
+ ):
40
+ inputs = tokenizer(
41
+ text,
42
+ return_tensors="pt",
43
+ truncation=True,
44
+ padding=True,
45
+ max_length=max_length,
46
+ )
47
+ inputs = {k: v.to(device) for k, v in inputs.items()}
48
+
49
+ outputs = model(**inputs)
50
+
51
+ probs = torch.sigmoid(outputs.logits[0]).cpu()
52
+
53
+ pred_ids = (probs >= threshold).nonzero(as_tuple=True)[0].tolist()
54
+ pred_labels = [ids2cat[i] for i in pred_ids]
55
+
56
+ return {
57
+ "labels": pred_labels,
58
+ "scores": {ids2cat[i]: float(probs[i]) for i in range(num_labels)},
59
+ }
60
+
61
+
62
+ if __name__ == "__main__":
63
+ text = """Macroscopic transport patterns of UAV traffic in 3D anisotropic wind fields: A constraint-preserving hybrid PINN-FVM approach""" + '\n\n' + """Macroscopic unmanned aerial vehicle (UAV) traffic organization in three-dimensional airspace faces significant challenges from static wind fields and complex obstacles. A critical difficulty lies in simultaneously capturing the strong anisotropy induced by wind while strictly preserving transport consistency and boundary semantics, which are often compromised in standard physics-informed learning approaches. To resolve this, we propose a constraint-preserving hybrid solver that integrates a physics-informed neural network for the anisotropic Eikonal value problem with a conservative finite-volume method for steady density transport. These components are coupled through an outer Picard iteration with under-relaxation, where the target condition is hard-encoded and strictly conservative no-flux boundaries are enforced during the transport step. We evaluate the framework on reproducible homing and point-to-point scenarios, effectively capturing value slices, induced-motion patterns, and steady density structures such as bands and bottlenecks. Ultimately, our perspective emphasizes the value of a reproducible computational framework supported by transparent empirical diagnostics to enable the traceable assessment of macroscopic traffic phenomena."""
64
+
65
+ result = predict(text, threshold=0.7)
66
+
67
+ print("Predicted labels:")
68
+ for label in result["labels"]:
69
+ print(label)
70
+
71
+ print("\nTop-10 scores:")
72
+ top_scores = sorted(result["scores"].items(), key=lambda x: x[1], reverse=True)[:10]
73
+ for label, score in top_scores:
74
+ print(f"{label}: {score:.4f}")
preprocess.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from os import PathLike
2
+ from transformers import AutoTokenizer, PreTrainedTokenizerBase, AutoConfig
3
+ from datasets import Dataset
4
+ from typing import Dict, Any, Tuple, List
5
+ from pathlib import Path
6
+ import json
7
+
8
+
9
+ def make_full_text_(title: str, abstract: str) -> str:
10
+ if abstract is None:
11
+ abstract = ""
12
+ return title + "\n\n" + abstract
13
+
14
+
15
+ def transform_(
16
+ examples: Dict[str, Any],
17
+ tokenizer: PreTrainedTokenizerBase,
18
+ categories_column: str,
19
+ cat2ids: Dict[str, int],
20
+ tokenizer_cfg: Dict[str, Any],
21
+ ):
22
+ # batched=True => examples это dict списков
23
+
24
+ full_texts = [
25
+ make_full_text_(title, abstract)
26
+ for title, abstract in zip(examples["title"], examples["abstract"])
27
+ ]
28
+
29
+ title_tokens = tokenizer(examples["title"], **tokenizer_cfg)
30
+ full_tokens = tokenizer(full_texts, **tokenizer_cfg)
31
+
32
+ return {
33
+ "title_input_ids": title_tokens["input_ids"],
34
+ "title_attention_mask": title_tokens["attention_mask"],
35
+
36
+ "full_input_ids": full_tokens["input_ids"],
37
+ "full_attention_mask": full_tokens["attention_mask"],
38
+
39
+ "labels_ids": [
40
+ [cat2ids[cat] for cat in categories]
41
+ for categories in examples[categories_column]
42
+ ],
43
+ }
44
+
45
+
46
+ def json_to_dataset_(data: Dict[str, Any], categories_column: str) -> Tuple[Dataset, List[str]]:
47
+ rows = []
48
+ cats_names = set()
49
+ for arxiv_id, fields in data.items():
50
+ row = {
51
+ "id": arxiv_id,
52
+ **fields
53
+ }
54
+ rows.append(row)
55
+ cats_names.update(fields[categories_column])
56
+
57
+ return Dataset.from_list(rows), list(cats_names)
58
+
59
+
60
+ def dataset_preprocess(
61
+ dataset_path: str | PathLike[str],
62
+ classifier_name: str,
63
+ categories_column: str):
64
+
65
+ if not Path(dataset_path).exists():
66
+ raise FileNotFoundError
67
+
68
+ with open(dataset_path, "r", encoding="utf-8") as f:
69
+ data = json.load(f)
70
+
71
+ dataset, categories = json_to_dataset_(data, categories_column)
72
+ categories = sorted(categories)
73
+
74
+ cat2ids = {}
75
+ ids2cat = {}
76
+
77
+ for idx, cat in enumerate(categories):
78
+ cat2ids[cat] = idx
79
+ ids2cat[idx] = cat
80
+
81
+ tokenizer = AutoTokenizer.from_pretrained(classifier_name)
82
+ model_cfg = AutoConfig.from_pretrained(classifier_name)
83
+
84
+ tokenizer_cfg = {
85
+ "truncation": True,
86
+ "padding": "max_length",
87
+ "max_length": model_cfg.max_position_embeddings
88
+ }
89
+
90
+ dataset = dataset.map(
91
+ lambda examples: transform_(
92
+ examples,
93
+ tokenizer,
94
+ categories_column,
95
+ cat2ids,
96
+ tokenizer_cfg
97
+ ),
98
+ batched=True,
99
+ batch_size=10_000,
100
+ remove_columns=dataset.column_names,
101
+ )
102
+
103
+ return {
104
+ "dataset": dataset,
105
+ "cat2ids": cat2ids,
106
+ "ids2cat": ids2cat,
107
+ }
train.py ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from collections import Counter, defaultdict
3
+ from os import PathLike
4
+ from typing import Any, Dict, List
5
+
6
+ from torch import nn
7
+ from transformers import Trainer, AutoModelForSequenceClassification, TrainingArguments
8
+ from peft import get_peft_model, LoraConfig, TaskType
9
+ from datasets import load_from_disk
10
+ from preprocess import dataset_preprocess
11
+ from pathlib import Path
12
+
13
+ import numpy as np
14
+ import torch
15
+
16
+
17
+ class BatchCollator:
18
+ def __init__(
19
+ self,
20
+ num_classes: int,
21
+ p_only_title: float = 0.4,
22
+ ):
23
+ self.num_classes = num_classes
24
+ self.p_only_title = p_only_title
25
+
26
+ def __call__(self, features: List[Dict[str, Any]]) -> Dict[str, Any]:
27
+ batch_size = len(features)
28
+
29
+ use_only_title = np.random.rand(batch_size) < self.p_only_title
30
+
31
+ input_ids = []
32
+ attention_mask = []
33
+
34
+ for i, feat in enumerate(features):
35
+
36
+ required_keys = [
37
+ 'title_input_ids',
38
+ 'title_attention_mask',
39
+ 'full_input_ids',
40
+ 'full_attention_mask',
41
+ 'labels_ids',
42
+ ]
43
+
44
+ if not all(k in feat for k in required_keys):
45
+ raise KeyError(f'{required_keys} must be in dataset elements!')
46
+
47
+ if use_only_title[i]:
48
+ input_ids.append(feat["title_input_ids"])
49
+ attention_mask.append(feat["title_attention_mask"])
50
+ else:
51
+ input_ids.append(feat["full_input_ids"])
52
+ attention_mask.append(feat["full_attention_mask"])
53
+
54
+ input_ids = torch.tensor(input_ids, dtype=torch.long)
55
+ attention_mask = torch.tensor(attention_mask, dtype=torch.long)
56
+
57
+ labels = torch.zeros((batch_size, self.num_classes), dtype=torch.float32)
58
+
59
+ for i, el in enumerate(features):
60
+ for label_id in el["labels_ids"]:
61
+ labels[i, label_id] = 1.0
62
+
63
+ return {
64
+ "input_ids": input_ids,
65
+ "attention_mask": attention_mask,
66
+ "labels": labels,
67
+ }
68
+
69
+ def get_dataset(
70
+ dataset_json_path: str | PathLike[str],
71
+ model_name: str,
72
+ categories_column: str
73
+ ) -> Dict[str, Any]:
74
+
75
+ save_path = Path('./data')
76
+
77
+ if (save_path / 'tokenized_dataset').exists():
78
+ dataset = load_from_disk(str(save_path / 'tokenized_dataset'))
79
+
80
+ with open(save_path / 'cat2ids.json', 'r') as f:
81
+ cat2ids = json.load(f)
82
+ cat2ids = {k: int(v) for k, v in cat2ids.items()}
83
+
84
+ with open(save_path / 'ids2cat.json', 'r') as f:
85
+ ids2cat = json.load(f)
86
+ ids2cat = {int(k): v for k, v in ids2cat.items()}
87
+
88
+ return {
89
+ "dataset": dataset,
90
+ "cat2ids": cat2ids,
91
+ "ids2cat": ids2cat,
92
+ }
93
+
94
+ result = dataset_preprocess(
95
+ dataset_json_path,
96
+ model_name,
97
+ categories_column
98
+ )
99
+ save_data(result, './data')
100
+
101
+ return result
102
+
103
+ def save_data(result: Dict[str, Any], save_path: str | PathLike[str]):
104
+ save_path = Path(save_path)
105
+ save_path.mkdir(parents=True, exist_ok=True)
106
+
107
+ if not (save_path / 'tokenized_dataset').exists():
108
+ dataset = result["dataset"]
109
+ dataset.save_to_disk(save_path / 'tokenized_dataset')
110
+
111
+ if not (save_path / 'cat2ids.json').exists():
112
+ with open(save_path / 'cat2ids.json', "w", encoding="utf-8") as f:
113
+ json.dump(result['cat2ids'], f, ensure_ascii=False, indent=4)
114
+
115
+ if not (save_path / 'ids2cat.json').exists():
116
+ with open(save_path / 'ids2cat.json', "w", encoding="utf-8") as f:
117
+ json.dump(result['ids2cat'], f, ensure_ascii=False, indent=4)
118
+
119
+ def compute_cats_weights(dataset, num_classes: int) -> torch.Tensor:
120
+ obj_per_cat = defaultdict(int)
121
+
122
+ for labels in dataset["labels_ids"]:
123
+ for label in labels:
124
+ obj_per_cat[label] += 1
125
+
126
+ num_obj = len(dataset)
127
+ pos_weights = []
128
+
129
+ for class_id in range(num_classes):
130
+ pos_count = obj_per_cat[class_id]
131
+
132
+ if pos_count == 0:
133
+ pos_weights.append(1.0)
134
+ else:
135
+ pos_weights.append((num_obj - pos_count) / pos_count)
136
+
137
+ return torch.tensor(pos_weights, dtype=torch.float32)
138
+
139
+
140
+ def compute_loss_func(outputs, labels, num_items_in_batch=None):
141
+ logits = outputs.logits
142
+ loss = nn.functional.binary_cross_entropy_with_logits(
143
+ input=logits,
144
+ target=labels,
145
+ pos_weight=pos_weights.to(logits.device)
146
+ )
147
+ return loss
148
+
149
+
150
+ if __name__ == '__main__':
151
+ dataset_path = Path('dataset.json')
152
+ model_name = 'oracat/bert-paper-classifier-arxiv'
153
+ output_model_path = Path(f'./checkpoints/{model_name.split("/")[-1]}/checkpoints')
154
+
155
+ categories_column = 'categories'
156
+
157
+ result = get_dataset(dataset_path, model_name, categories_column)
158
+
159
+ num_cats = len(result['cat2ids'])
160
+ print(f'num_cats: {num_cats}')
161
+
162
+ dataset = result['dataset']
163
+ dataset = dataset.train_test_split(test_size=0.1, seed=123)
164
+ train_dataset, test_dataset = dataset['train'], dataset['test']
165
+
166
+
167
+ #####
168
+ data = []
169
+ for x in train_dataset['labels_ids']:
170
+ data.extend(x)
171
+
172
+ cnt = Counter(data)
173
+ print(f'num classes in train: {len(cnt.keys())}')
174
+
175
+ data = []
176
+ for x in test_dataset['labels_ids']:
177
+ data.extend(x)
178
+
179
+ cnt = Counter(data)
180
+ print(f'num classes in test: {len(cnt.keys())}')
181
+ #####
182
+
183
+ model = AutoModelForSequenceClassification.from_pretrained(
184
+ model_name,
185
+ num_labels=num_cats,
186
+ problem_type='multi_label_classification',
187
+ ignore_mismatched_sizes=True
188
+ )
189
+
190
+ pos_weights = compute_cats_weights(train_dataset, num_cats)
191
+
192
+ assert pos_weights.shape[0] == num_cats
193
+
194
+ lora_config = LoraConfig(
195
+ modules_to_save=['classifier'],
196
+ use_rslora=True,
197
+ r=16,
198
+ lora_alpha=32,
199
+ lora_dropout=0.05,
200
+ task_type=TaskType.SEQ_CLS
201
+ )
202
+
203
+ model = get_peft_model(model, lora_config)
204
+ model.print_trainable_parameters()
205
+
206
+ training_args = TrainingArguments(
207
+ per_device_train_batch_size=64,
208
+ per_device_eval_batch_size=64,
209
+
210
+ output_dir=output_model_path,
211
+ logging_dir=output_model_path / "runs",
212
+
213
+ num_train_epochs=20,
214
+ learning_rate=5e-4,
215
+ lr_scheduler_type='cosine',
216
+ warmup_steps=10,
217
+ optim='adamw_torch_fused',
218
+ weight_decay=0.001,
219
+
220
+ gradient_accumulation_steps=4,
221
+ bf16=True,
222
+
223
+ logging_strategy='epoch',
224
+ eval_strategy='epoch',
225
+
226
+ load_best_model_at_end=True,
227
+ save_only_model=False,
228
+ save_total_limit=2,
229
+ save_strategy='epoch',
230
+ disable_tqdm=False,
231
+ remove_unused_columns=False,
232
+ seed=42,
233
+ dataloader_num_workers=4
234
+ )
235
+
236
+ collator = BatchCollator(
237
+ num_classes=num_cats,
238
+ p_only_title=0.4
239
+ )
240
+
241
+ trainer = Trainer(
242
+ model=model,
243
+ args=training_args,
244
+ train_dataset=train_dataset,
245
+ eval_dataset=test_dataset,
246
+ data_collator=collator,
247
+ compute_loss_func=compute_loss_func,
248
+ )
249
+
250
+ trainer.train()
251
+
252
+ model.save_pretrained(output_model_path / 'final_model')