Skip to content

Commit ec1b4d6

Browse files
authored
New model!
. Была добавлена новая модель Turbo. Данная модель была обучена на 200 гб размеченных разными пайплайнами текстов. Размер модели сопоставим с medium_poetry, но качество выше big_poetry. Метрики: ruaccent_big -> 0.93 avg ruaccent_turbo -> 0.95 avg 3. Отказ от собственного тяжеловесного пайплайна морфологического анализатора, в сторону проекта Ильи Козиева rupostagger. 4. Доработка пайплайна с нейросетью для расстановки ударений в обычных слов 5. Исправлены некоторые ошибки
1 parent 175c6c8 commit ec1b4d6

File tree

11 files changed

+957
-17
lines changed

11 files changed

+957
-17
lines changed

README.md

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,24 @@ RUAccent - это библиотека для автоматической ра
1616
```
1717
## Параметры работы
1818

19-
load(omograph_model_size='big_poetry', use_dictionary=True, custom_dict={})
19+
load(omograph_model_size='turbo', use_dictionary=True, custom_dict={}, device="CPU", workdir=None)
2020

21-
- На данный момент доступно 6 моделей. **big** (рекомендуется к использованию), **medium** и **small**. Рекомендуются к использованию модели версии **poetry**. Их названия **big_poetry**, **medium_poetry**, **small_poetry**.
22-
- Модель **big** имеет 178 миллионов параметров, **medium** 85 миллионов, а **small** 12 миллионов
21+
- На данный момент доступно 4 модели - **turbo**, **big_poetry**, **medium_poetry**, **small_poetry**
2322
- Переменная **use_dictionary** отвечает за загрузку всего словаря (требуется больше ОЗУ), иначе все ударения расставляет нейросеть.
2423
- Функция **custom_dict** отвечает за добавление своих вариантов ударений в словарь. Формат такой: `{'слово': 'сл+ово с удар+ением'}`
25-
26-
**Для работы требуется 5 гигабайт ОЗУ**
24+
- Выбор устройства CPU или CUDA. **Для работы с CUDA требуется установить onnxruntime-gpu и CUDA.**
25+
- workdir - принимает строку. Является путём, куда скачиваются модели.
26+
27+
**Для стабильной работы требуется минимум 3 гигабайта ОЗУ**
2728
## Пример использования
2829
```python
2930
from ruaccent import RUAccent
3031

3132
accentizer = RUAccent()
32-
accentizer.load(omograph_model_size='big_poetry', use_dictionary=True)
33+
accentizer.load(omograph_model_size='turbo', use_dictionary=True)
3334

3435
text = 'на двери висит замок.'
3536
print(accentizer.process_all(text))
36-
37-
text = 'ежик нашел в лесу ягоды.'
38-
print(accentizer.process_yo(text))
3937
```
4038

4139
Файлы моделей и словарей располагаются по [ссылке](https://huggingface.co/ruaccent/accentuator). Мы будем признательны фидбеку на [telegram аккаунт](https://t.me/chckdskeasfsd)

ruaccent/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Russian accentizer"""
2+
3+
__version__ = "1.5.6.1"
4+
5+
6+
from .ruaccent import RUAccent

ruaccent/accent_model.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import numpy as np
2+
import json
3+
from onnxruntime import InferenceSession
4+
from .char_tokenizer import CharTokenizer
5+
6+
def softmax(x):
7+
e_x = np.exp(x - np.max(x))
8+
return e_x / e_x.sum(axis=-1, keepdims=True)
9+
10+
class AccentModel:
11+
def __init__(self) -> None:
12+
pass
13+
14+
def load(self, path, device="CPU"):
15+
self.session = InferenceSession(f"{path}/model.onnx", providers=["CUDAExecutionProvider" if device == "CUDA" else "CPUExecutionProvider"])
16+
17+
with open(f"{path}/config.json", "r") as f:
18+
self.id2label = json.load(f)["id2label"]
19+
self.tokenizer = CharTokenizer.from_pretrained(path)
20+
21+
def render_stress(self, text, pred):
22+
text = list(text)
23+
i = 0
24+
for chunk in pred:
25+
if chunk['label'] != "NO" and chunk['label'] != "STRESS_SECONDARY" and chunk["score"] >= 0.55:
26+
text[i - 1] = "+" + text[i - 1]
27+
i += 1
28+
text = "".join(text)
29+
return text
30+
31+
def put_accent(self, word):
32+
inputs = self.tokenizer(word, return_tensors="np")
33+
inputs = {k: v.astype(np.int64) for k, v in inputs.items()}
34+
outputs = self.session.run(None, inputs)
35+
output_names = {output_key.name: idx for idx, output_key in enumerate(self.session.get_outputs())}
36+
logits = outputs[output_names["logits"]]
37+
probabilities = softmax(logits)
38+
scores = np.max(probabilities, axis=-1)[0]
39+
labels = np.argmax(logits, axis=-1)[0]
40+
pred_with_scores = [{'label': self.id2label[str(label)], 'score': float(score)}
41+
for label, score in zip(labels, scores)]
42+
43+
stressed_word = self.render_stress(word, pred_with_scores)
44+
45+
return stressed_word

ruaccent/char_tokenizer.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import os
2+
from typing import Optional, Tuple, List
3+
from collections import OrderedDict
4+
5+
from transformers import PreTrainedTokenizer
6+
7+
8+
def load_vocab(vocab_file):
9+
vocab = OrderedDict()
10+
with open(vocab_file, "r", encoding="utf-8") as reader:
11+
tokens = reader.readlines()
12+
for index, token in enumerate(tokens):
13+
token = token.rstrip("\n")
14+
vocab[token] = index
15+
return vocab
16+
17+
18+
class CharTokenizer(PreTrainedTokenizer):
19+
vocab_files_names = {"vocab_file": "vocab.txt"}
20+
21+
def __init__(
22+
self,
23+
vocab_file=None,
24+
pad_token="[pad]",
25+
unk_token="[unk]",
26+
bos_token="[bos]",
27+
eos_token="[eos]",
28+
do_lower_case=False,
29+
*args,
30+
**kwargs
31+
):
32+
self.vocab = load_vocab(vocab_file)
33+
super().__init__(
34+
pad_token=pad_token,
35+
unk_token=unk_token,
36+
bos_token=bos_token,
37+
eos_token=eos_token,
38+
do_lower_case=do_lower_case,
39+
**kwargs
40+
)
41+
self.do_lower_case = do_lower_case
42+
43+
@property
44+
def vocab_size(self):
45+
return len(self.vocab)
46+
47+
def get_vocab(self):
48+
return dict(self.vocab)
49+
50+
def _convert_token_to_id(self, token):
51+
if self.do_lower_case:
52+
token = token.lower()
53+
return self.vocab.get(token, self.vocab[self.unk_token])
54+
55+
def _convert_id_to_token(self, index):
56+
return self.ids_to_tokens[index]
57+
58+
def _tokenize(self, text):
59+
if self.do_lower_case:
60+
text = text.lower()
61+
return list(text)
62+
63+
def convert_tokens_to_string(self, tokens):
64+
return "".join(tokens)
65+
66+
def build_inputs_with_special_tokens(
67+
self,
68+
token_ids_0: List[int],
69+
token_ids_1: Optional[List[int]] = None
70+
) -> List[int]:
71+
bos = [self.bos_token_id]
72+
eos = [self.eos_token_id]
73+
return bos + token_ids_0 + eos
74+
75+
def get_special_tokens_mask(
76+
self,
77+
token_ids_0: List[int],
78+
token_ids_1: Optional[List[int]] = None
79+
) -> List[int]:
80+
return [1] + ([0] * len(token_ids_0)) + [1]
81+
82+
def create_token_type_ids_from_sequences(
83+
self,
84+
token_ids_0: List[int],
85+
token_ids_1: Optional[List[int]] = None
86+
) -> List[int]:
87+
return (len(token_ids_0) + 2) * [0]
88+
89+
def save_vocabulary(
90+
self,
91+
save_directory: str,
92+
filename_prefix: Optional[str] = None
93+
) -> Tuple[str]:
94+
assert os.path.isdir(save_directory)
95+
vocab_file = os.path.join(
96+
save_directory,
97+
(filename_prefix + "-" if filename_prefix else "") +
98+
self.vocab_files_names["vocab_file"]
99+
)
100+
index = 0
101+
with open(vocab_file, "w", encoding="utf-8") as writer:
102+
for token, token_index in sorted(self.vocab.items(), key=lambda kv: kv[1]):
103+
assert index == token_index
104+
writer.write(token + "\n")
105+
index += 1
106+
return (vocab_file,)

ruaccent/omograph_model.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import numpy as np
2+
from onnxruntime import InferenceSession
3+
from transformers import AutoTokenizer
4+
import re
5+
6+
7+
class OmographModel:
8+
def __init__(self):
9+
self.special_words = ['балчуга', 'вертела', 'волоки', 'волоку', 'воронью', 'выбродите', 'вывозите', 'выносите', 'выноситесь', 'выходите', 'железы', 'начала', 'округа', 'перепела', 'развитая', 'развитого', 'развитое', 'развитой', 'развитом', 'развитому', 'развитою', 'развитую', 'развитые', 'развитым', 'развитыми', 'развитых', 'сторожа', 'сторожи', 'сторожу', 'удало', 'начался', 'началась', 'началось', 'бутиках', 'ожила', 'создало', 'коротки', 'проклята', 'роженица', 'роженицы', 'рожениц', 'роженице', 'роженицам', 'роженицу', 'роженицей', 'роженицею', 'роженицами', 'роженицах', 'пристава', 'приставов', 'приставам', 'приставами', 'приставах', 'пережитое', 'пережитого', 'пережитые', 'пережитых', 'пережитому', 'пережитым', 'пережитыми', 'пережитом', 'нипоняла']
10+
11+
12+
def load(self, path, device="CPU"):
13+
self.session = InferenceSession(f"{path}/model.onnx", providers=["CUDAExecutionProvider" if device == "CUDA" else "CPUExecutionProvider"])
14+
self.tokenizer = AutoTokenizer.from_pretrained(path)
15+
16+
def softmax(self, x):
17+
e_x = np.exp(x - np.max(x))
18+
return e_x / e_x.sum()
19+
20+
def group_words(self, words):
21+
groups = {}
22+
for word in words:
23+
parts = word.replace('+', '')
24+
key = parts
25+
group = groups.setdefault(key, [])
26+
group.append(word)
27+
28+
result = []
29+
for group in groups.values():
30+
has_special_word = any(word.replace('+', '') in self.special_words for word in group)
31+
if has_special_word and len(group) > 3:
32+
subgroups = [group[i:i+3] for i in range(0, len(group), 3)]
33+
result.extend(subgroups)
34+
elif len(group) > 3 and len(group) % 2 == 0:
35+
subgroups = [group[i:i+2] for i in range(0, len(group), 2)]
36+
result.extend(subgroups)
37+
else:
38+
result.append(group)
39+
40+
return result
41+
42+
def transfer_grouping(self, grouped_list, target_list):
43+
new_grouped_list = []
44+
start_index = 0
45+
for group in grouped_list:
46+
group_length = len(group)
47+
new_group = target_list[start_index:start_index + group_length]
48+
new_grouped_list.append(new_group)
49+
start_index += group_length
50+
return new_grouped_list
51+
52+
def classify(self, texts, hypotheses):
53+
hypotheses_probs = []
54+
preprocessed_texts = [re.sub(r'\s+(?=(?:[,.?!:;…]))', r'', text) for text in texts]
55+
if len(hypotheses) % 2 != 0:
56+
#print("NO_BATCH")
57+
outs = []
58+
grouped_h = self.group_words(hypotheses)
59+
grouped_t = self.transfer_grouping(grouped_h, preprocessed_texts)
60+
for h, t in zip(grouped_h, grouped_t):
61+
probs = []
62+
for hp in h:
63+
inputs = self.tokenizer(t[0], hp, max_length=512, truncation=True, return_tensors="np")
64+
inputs = {k: v.astype(np.int64) for k, v in inputs.items()}
65+
outputs = self.session.run(None, inputs)[0]
66+
outputs = self.softmax(outputs)
67+
prob_label_is_true = [float(p[1]) for p in outputs][0]
68+
probs.append(prob_label_is_true)
69+
#print(h, prob_label_is_true)
70+
outs.append(h[probs.index(max(probs))])
71+
return outs
72+
else:
73+
inputs = self.tokenizer(preprocessed_texts, hypotheses, return_tensors="np", padding=True, truncation=True, max_length=512)
74+
inputs = {k: v.astype(np.int64) for k, v in inputs.items()}
75+
76+
outputs = self.session.run(None, inputs)[0]
77+
outputs = self.softmax(outputs)
78+
#print(hypotheses)
79+
preprocessed_texts = [(preprocessed_texts[i], preprocessed_texts[i+1]) for i in range(0, len(preprocessed_texts), 2)]
80+
hypotheses = [(hypotheses[i], hypotheses[i+1]) for i in range(0, len(hypotheses), 2)]
81+
82+
for i in range(len(texts)):
83+
prob_label_is_true = float(outputs[i][1])
84+
hypotheses_probs.append(prob_label_is_true)
85+
86+
hypotheses_probs = [(hypotheses_probs[i], hypotheses_probs[i+1]) for i in range(0, len(hypotheses_probs), 2)]
87+
outs = []
88+
for pair1, pair2 in zip(hypotheses, hypotheses_probs):
89+
outs.append(pair1[pair2.index(max(pair2))])
90+
return outs

0 commit comments

Comments
 (0)