File size: 6,669 Bytes
2ed761d
102a3a5
 
 
 
 
79fb46e
102a3a5
 
0011926
2ed761d
0011926
 
 
 
102a3a5
2ed761d
0011926
2ed761d
0011926
 
 
2ed761d
 
 
0011926
 
 
102a3a5
120a364
2ed761d
 
 
0011926
 
102a3a5
120a364
0011926
79fb46e
 
2ed761d
59d5f39
2ed761d
102a3a5
951c20c
4e94bf9
0011926
951c20c
 
4e94bf9
120a364
951c20c
120a364
0011926
79fb46e
951c20c
120a364
 
0011926
4e94bf9
0011926
 
951c20c
4e94bf9
 
 
07b63ff
0011926
 
3cc820b
0011926
 
120a364
6eb3f68
951c20c
4e94bf9
0011926
 
 
3cc820b
 
 
 
 
 
 
 
 
 
79fb46e
2ed761d
951c20c
120a364
951c20c
0011926
102a3a5
 
0011926
951c20c
 
 
4e94bf9
0011926
951c20c
 
0011926
951c20c
 
 
 
0011926
2ed761d
951c20c
102a3a5
 
2e476d1
 
17a8a7f
951c20c
 
 
 
 
 
79fb46e
 
 
951c20c
 
4e94bf9
102a3a5
951c20c
 
 
102a3a5
 
4e94bf9
102a3a5
951c20c
4e94bf9
79fb46e
951c20c
102a3a5
951c20c
79fb46e
4e94bf9
102a3a5
 
 
 
951c20c
4e94bf9
a3e69f6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
import re
import torch
import gradio as gr
from transformers import (
    AutoModelForTokenClassification,
    AutoModelForSequenceClassification,
    AutoTokenizer,
)

ID2LABEL = {
    0: "O",
    1: "B-TOPONYM", 2: "I-TOPONYM",
    3: "B-MEDICINE", 4: "I-MEDICINE",
    5: "B-SYMPTOM", 6: "I-SYMPTOM",
    7: "B-ALLERGEN", 8: "I-ALLERGEN",
    9: "B-BODY_PART", 10: "I-BODY_PART",
}
LABEL2ID = {v: k for k, v in ID2LABEL.items()}

REL_LABELS = ["has_symptom", "has_medicine", "no_relation"]
REL_ID2LABEL = {i: l for i, l in enumerate(REL_LABELS)}
REL_LABEL2ID = {l: i for i, l in enumerate(REL_LABELS)}

def load_models():
    ner_model = AutoModelForTokenClassification.from_pretrained(
        "DanielNRU/pollen-ner",
        num_labels=len(ID2LABEL),
        id2label=ID2LABEL,
        label2id=LABEL2ID,
    )
    ner_tokenizer = AutoTokenizer.from_pretrained("DeepPavlov/rubert-base-cased")
    re_model = AutoModelForSequenceClassification.from_pretrained(
        "DanielNRU/pollen-re",
        num_labels=len(REL_LABELS),
        id2label=REL_ID2LABEL,
        label2id=REL_LABEL2ID,
    )
    re_tokenizer = AutoTokenizer.from_pretrained("DeepPavlov/rubert-base-cased")
    ner_model.eval()
    re_model.eval()
    return ner_model, ner_tokenizer, re_model, re_tokenizer

ner_model, ner_tokenizer, re_model, re_tokenizer = load_models()

def split_sentences(text):
    return [s.strip() for s in re.split(r"[.!?]", text) if s.strip()]

def predict_entities(text):
    inputs = ner_tokenizer(text, return_tensors="pt", return_offsets_mapping=True, truncation=True, max_length=512)
    offset_mapping = inputs.pop("offset_mapping")[0]
    with torch.no_grad():
        preds = ner_model(**inputs).logits.argmax(dim=-1)[0]
    entities = []
    current = None
    for pred, (start, end) in zip(preds, offset_mapping):
        start, end = int(start), int(end)
        if start == 0 and end == 0:
            continue
        label = ID2LABEL[pred.item()]
        if label.startswith("B-"):
            if current:
                entities.append(current)
            current = {"text": text[start:end], "label": label[2:], "start": start, "end": end}
        elif label.startswith("I-") and current and label[2:] == current["label"]:
            current["text"] += text[start:end]
            current["end"] = end
        else:
            if current:
                entities.append(current)
            current = None
    if current:
        entities.append(current)
    return entities

def insert_entity_markers(text, ent1, ent2):
    if ent1["start"] < ent2["start"]:
        first, second = ent1, ent2
    else:
        first, second = ent2, ent1
    t = (text[:second["start"]]
         + f"[{second['label']}]"
         + text[second["start"]:second["end"]]
         + f"[/{second['label']}]"
         + text[second["end"]:])
    t = (t[:first["start"]]
         + f"[{first['label']}]"
         + t[first["start"]:first["end"]]
         + f"[/{first['label']}]"
         + t[first["end"]:])
    return t

def predict_relations(text, entities):
    relations = []
    for sent in split_sentences(text):
        sent_start = text.find(sent)
        if sent_start == -1:
            continue
        sent_end = sent_start + len(sent)
        ents = [e for e in entities if e["start"] >= sent_start and e["end"] <= sent_end]
        for ent1 in ents:
            for ent2 in ents:
                if ent1 is ent2:
                    continue
                if ent1["label"] == "BODY_PART" and ent2["label"] in ["SYMPTOM", "MEDICINE"]:
                    inputs = re_tokenizer(insert_entity_markers(text, ent1, ent2), return_tensors="pt", truncation=True, max_length=256)
                    with torch.no_grad():
                        pred = re_model(**inputs).logits.argmax(-1).item()
                    rel = REL_ID2LABEL[pred]
                    if rel != "no_relation":
                        relations.append({"head": ent1, "tail": ent2, "relation": rel})
    return relations

def analyze_text(text):
    if not text or not text.strip():
        return [], "", "", "", ""
    entities = predict_entities(text)
    relations = predict_relations(text, entities)
    highlights = []
    used = set()
    for e in entities:
        s = (e["start"], e["end"], e["label"])
        if s not in used:
            highlights.append((e["text"], e["label"]))
            used.add(s)
    toponyms = [e["text"] for e in entities if e["label"] == "TOPONYM"]
    medicines = [e["text"] for e in entities if e["label"] == "MEDICINE"]
    allergens = [e["text"] for e in entities if e["label"] == "ALLERGEN"]
    symptoms = [f"{r['head']['text']} {r['tail']['text']}" for r in relations if r["relation"] == "has_symptom" and r["tail"]["label"] == "SYMPTOM"]
    return highlights, ", ".join(toponyms), ", ".join(medicines), ", ".join(symptoms), ", ".join(allergens)

EXAMPLES = [
    ["В Московской области у меня началась аллергия на пыльцу березы, потекли глаза, нос, принимаю Зиртек и Назонекс."],
    ["У ребенка в Новокузнецке чешутся глаза, уши и течет нос, врач прописал Кромогексал, Назонекс в нос."],
    ["В Санкт-Петербурге началось цветение ольхи, сильная реакция, принимаю Эриус, но глаза все равно слезятся."],
]

with gr.Blocks() as demo:
    gr.Markdown("## PollenNER — извлечение сущностей и отношений")
    gr.Markdown("Извлечение топонимов, лекарств, аллергенов, частей тела и симптомов из сообщений пользователей Пыльца Club.")
    with gr.Row():
        with gr.Column():
            input_text = gr.Textbox(label="Текст", lines=5, placeholder="Введите текст для анализа...")
            run_btn = gr.Button("Анализировать")
            gr.Examples(examples=EXAMPLES, inputs=input_text)
        with gr.Column():
            out_highlight = gr.HighlightedText(label="Сущности")
            out_toponyms = gr.Textbox(label="Топонимы")
            out_medicines = gr.Textbox(label="Медицинские препараты")
            out_symptoms = gr.Textbox(label="Симптомы")
            out_allergens = gr.Textbox(label="Аллергены")
    run_btn.click(fn=analyze_text, inputs=input_text, outputs=[out_highlight, out_toponyms, out_medicines, out_symptoms, out_allergens])

demo.launch()