Bjg6742635 commited on
Commit
c78713f
·
1 Parent(s): dccd91b

Initial deploy

Browse files
Files changed (3) hide show
  1. Dockerfile +12 -10
  2. app.py +164 -0
  3. requirements.txt +11 -2
Dockerfile CHANGED
@@ -1,20 +1,22 @@
1
- FROM python:3.13.5-slim
2
 
3
  WORKDIR /app
4
 
5
- RUN apt-get update && apt-get install -y \
6
- build-essential \
7
- curl \
8
- git \
9
- && rm -rf /var/lib/apt/lists/*
10
 
11
- COPY requirements.txt ./
12
- COPY src/ ./src/
13
 
14
- RUN pip3 install -r requirements.txt
 
 
 
 
 
15
 
16
  EXPOSE 8501
17
 
18
  HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
19
 
20
- ENTRYPOINT ["streamlit", "run", "src/streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"]
 
1
+ FROM python:3.11-slim
2
 
3
  WORKDIR /app
4
 
5
+ # Сначала копируем requirements.txt, чтобы использовать кэш
6
+ COPY requirements.txt .
 
 
 
7
 
8
+ # Устанавливаем зависимости
9
+ RUN pip3 install --no-cache-dir -r requirements.txt
10
 
11
+ RUN python -c "import spacy; spacy.cli.download('ru_core_news_lg')"
12
+ RUN python -c "import nltk; nltk.download('punkt_tab', download_dir='/usr/local/share/nltk_data')"
13
+ RUN python -c "import nltk; nltk.download('stopwords')"
14
+
15
+ # Копируем остальные файлы
16
+ COPY . .
17
 
18
  EXPOSE 8501
19
 
20
  HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
21
 
22
+ ENTRYPOINT ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]
app.py ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ import streamlit as st
4
+ import pandas as pd
5
+ import numpy as np
6
+ from datasets import load_dataset, concatenate_datasets
7
+ from sklearn.feature_extraction.text import TfidfVectorizer
8
+ from sklearn.metrics.pairwise import cosine_similarity
9
+ import torch
10
+ from transformers import AutoTokenizer, AutoModelForQuestionAnswering
11
+ import spacy
12
+ from nltk.corpus import stopwords
13
+ from nltk.tokenize import word_tokenize
14
+ import re
15
+ from bs4 import BeautifulSoup
16
+
17
+ # === Загрузка и подготовка данных ===
18
+
19
+ @st.cache_resource
20
+ def load_data():
21
+ # Загрузка датасета
22
+ data = load_dataset('Romyx/ru_QA_school_history', split='train')
23
+ df = pd.DataFrame(data)
24
+ df['Pt_question'] = df['question'].apply(preprocess_text)
25
+ df['Pt_answer'] = df['answer'].apply(preprocess_text)
26
+ return df
27
+
28
+ @st.cache_resource
29
+ def load_model_and_tokenizer():
30
+ # Загрузка предобученной модели вопрос-ответа (например, SberQuad)
31
+ model_name = "AlexKay/xlm-roberta-large-qa-multilingual-finedtuned-ru" # замените на нужную модель, например, "bert-base-uncased"
32
+ tokenizer = AutoTokenizer.from_pretrained(model_name)
33
+ model = AutoModelForQuestionAnswering.from_pretrained(model_name)
34
+ return tokenizer, model
35
+
36
+ @st.cache_resource
37
+ def build_vectorizer(_df):
38
+ combined_texts = _df['Pt_question'].tolist() + _df['Pt_answer'].tolist()
39
+ vectorizer = TfidfVectorizer()
40
+ tfidf_matrix = vectorizer.fit_transform(combined_texts)
41
+ return vectorizer, tfidf_matrix
42
+
43
+ # === Предобработка текста ===
44
+
45
+ # Загрузка Spacy модели
46
+ nlp = spacy.load('ru_core_news_lg')
47
+ stop_words = set(stopwords.words('russian'))
48
+
49
+ cache_dict = {}
50
+
51
+ def get_norm_form(word):
52
+ if word in cache_dict:
53
+ return cache_dict[word]
54
+ norm_form = nlp(word)[0].lemma_
55
+ cache_dict[word] = norm_form
56
+ return norm_form
57
+
58
+ def remove_html_tags(text):
59
+ soup = BeautifulSoup(text, 'html.parser')
60
+ return soup.text
61
+
62
+ def preprocess_text(text):
63
+ if pd.isna(text) or text is None:
64
+ return ""
65
+ text = remove_html_tags(text)
66
+ text = text.lower()
67
+
68
+ # Обработка знаков препинания
69
+ text = re.sub(r'([^\w\s-]|_)', r' \1 ', text)
70
+ text = re.sub(r'\s+', ' ', text)
71
+ text = re.sub(r'(\w+)-(\w+)', r'\1 \2', text)
72
+ text = re.sub(r'(\d+)(г|кг|см|м|мм|л|мл)', r'\1 \2', text)
73
+
74
+ # Удаление всего, кроме букв, цифр и пробелов
75
+ text = re.sub(r'[^\w\s]', '', text)
76
+
77
+ tokens = word_tokenize(text)
78
+ tokens = [token for token in tokens if token not in stop_words]
79
+ tokens = [get_norm_form(token) for token in tokens]
80
+
81
+ words_to_remove = {"ответ", "new"}
82
+ tokens = [token for token in tokens if token not in words_to_remove]
83
+
84
+ return ' '.join(tokens)
85
+
86
+ # === Основная функция получения ответа ===
87
+ def get_answer_from_qa_model(user_question, df, vectorizer, tfidf_matrix, model, tokenizer):
88
+ processed = preprocess_text(user_question)
89
+ user_vec = vectorizer.transform([processed])
90
+
91
+ similarities = cosine_similarity(user_vec, tfidf_matrix).flatten()
92
+
93
+ # Проверка, что similarities не пустой
94
+ if len(similarities) == 0:
95
+ return "Тема не входит в программу этих классов."
96
+
97
+ best_match_idx = similarities.argmax()
98
+ best_score = similarities[best_match_idx]
99
+
100
+ if best_score > 0.1:
101
+ # Проверка, что индекс не выходит за границы
102
+ if best_match_idx >= len(df):
103
+ return "Тема не входит в программу этих классов."
104
+
105
+ context = df.iloc[best_match_idx]['answer']
106
+ question = user_question
107
+
108
+ inputs = tokenizer(question, context, return_tensors="pt", truncation=True, padding=True)
109
+
110
+ with torch.no_grad():
111
+ outputs = model(**inputs)
112
+
113
+ start_scores = outputs.start_logits
114
+ end_scores = outputs.end_logits
115
+
116
+ # Проверка на корректность размера логитов
117
+ if len(start_scores.shape) == 2:
118
+ start_idx = torch.argmax(start_scores, dim=1)[0].item()
119
+ end_idx = torch.argmax(end_scores, dim=1)[0].item()
120
+ else:
121
+ start_idx = torch.argmax(start_scores).item()
122
+ end_idx = torch.argmax(end_scores).item()
123
+
124
+ # Проверка, что индексы не выходят за пределы
125
+ seq_len = inputs['input_ids'].shape[1]
126
+ if start_idx >= seq_len or end_idx >= seq_len or start_idx > end_idx:
127
+ return "Ответ не найден."
128
+
129
+ answer = tokenizer.decode(inputs['input_ids'][0][start_idx:end_idx+1], skip_special_tokens=True)
130
+ else:
131
+ answer = "Извините, я не понимаю вопрос."
132
+
133
+ return answer
134
+
135
+ # === Интерфейс Streamlit ===
136
+
137
+ def main():
138
+ st.title("🤖 ИИ-ассистент по истории (на основе вопрос-ответа)")
139
+
140
+ st.write("Задайте вопрос, и я постараюсь найти на него ответ из базы.")
141
+
142
+ # Загрузка данных и модели
143
+ df = load_data()
144
+ tokenizer, model = load_model_and_tokenizer()
145
+ vectorizer, tfidf_matrix = build_vectorizer(df)
146
+
147
+ # Поле ввода вопроса
148
+ user_input = st.text_input("Введите ваш вопрос:")
149
+
150
+ if st.button("Получить ответ"):
151
+ if user_input.strip():
152
+ with st.spinner("Ищем ответ..."):
153
+ response = get_answer_from_qa_model(
154
+ user_input, df, vectorizer, tfidf_matrix, model, tokenizer
155
+ )
156
+ st.success("Ответ:")
157
+ st.write(response)
158
+ else:
159
+ st.warning("Пожалуйста, введите вопрос.")
160
+
161
+ if __name__ == "__main__":
162
+ main()
163
+
164
+
requirements.txt CHANGED
@@ -1,3 +1,12 @@
1
- altair
 
 
2
  pandas
3
- streamlit
 
 
 
 
 
 
 
 
1
+
2
+ streamlit
3
+ openai
4
  pandas
5
+ numpy
6
+ datasets
7
+ scikit-learn
8
+ torch --index-url https://download.pytorch.org/whl/cpu
9
+ transformers
10
+ spacy
11
+ nltk
12
+ beautifulsoup4