Пишем чат-бот на Python + PostgreSQL и Telegram

от автора

Пошаговое руководство написания чат-бота на языке Python.

  • Установим Python и библиотеки;

  • Получим вопросы и ответы из БД PostgreSQL;

  • Подключим морфологию;

  • Подключим чат-бот к каналу Telegram.

Colaboratory от Google

Изучение Python можно начать используя сервис Colaboratory от Google, или просто Colab. Сервис позволяет писать и выполнять код Python в браузере, не требуя собственного сервера.

Пример кода. Вопросы и ответы для чат-бота подгрузим с https://drive.google.com из текстового файла

# Однократно после запуска виртуальной машины устанавливаем библиотеки pymorphy2 и numpy !pip install pymorphy2 numpy  # ----------------------------  # подключим библиотеки import  csv import pymorphy2 import re  morph = pymorphy2.MorphAnalyzer(lang='ru')  # Массив вопросов questions=[]   # Массив ответов answer=[]   # Подключаем файл с Google диска, содержащий вопросы/ответы # Есть ли жизнь на марсе;Есть  with open("/content/drive/MyDrive/robo-bot/question.txt", "r") as f_obj:   reader = csv.reader(f_obj)   for row in reader:     s=" ".join(row)     r=s.split(';')     questions.append(r[0])     answer.append(r[1])  # выведем список вопросов и ответов print (questions) print (answer)  

Запуск в Production

Наигравшись с кодом в Colaboratory и освоив Python развернем систему на боевом сервере Debian

Установим Python и PIP (установщик пакетов).

Так как Debian не самый новый, устанавливается версия 3.5

aptitude install python3 python3-pip  # обновим пакеты если они были установлены ранее pip3 install --upgrade setuptools pip

Установим необходимые пакеты Python

# Из за устаревшей версии Debian установить psycopg2 не удалось, поставлен скомпилированный psycopg2-binary  # Библиотека psycopg2 нужна для подключения к базе данных PostgreSQL  # pip3 install psycopg2  pip3 install psycopg2-binary scikit-learn numpy pymorphy2

Пишем код в файле Chat_bot.py

# Импортируем библиотеки import pymorphy2 import re import psycopg2 import sklearn import numpy as np   # Подключаемся к PostgreSQL conn = psycopg2.connect(dbname='energy', user='mao', password='darin', host='localhost') cursor = conn.cursor()  # Настраиваем язык для библиотеки морфологии morph = pymorphy2.MorphAnalyzer(lang='ru')  # объявляем массив кодов ответов и ответов answer_id=[]  answer = dict()  # получаем из PostgreSQL список ответов и проиндексируем их. # Работая с PostgreSQL обращаемся к схеме app, в которой находятся таблицы с данными cursor.execute('SELECT id, answer FROM app.chats_answer;') records = cursor.fetchall() for row in records: answer[row[0]]=row[1]    

Структура таблицы ответов chats_answer, формат SQL

CREATE TABLE app.chats_answer (   id SERIAL,   answer VARCHAR(512),   CONSTRAINT chats_answer_pkey PRIMARY KEY(id) )  WITH (oids = false);  ALTER TABLE app.chats_answer   OWNER TO mao;

id

answer

1

Мне 45 лет

2

Да, я готов об этом поговорить

3

Я тоже хочу спать

Структура таблицы вопросов chats_question, формат SQL. Каждый вопрос связан с кодом ответа.

CREATE TABLE app.chats_question (   id SERIAL,   question VARCHAR(512),   answer_id INTEGER,   CONSTRAINT chats_question_pkey PRIMARY KEY(id) )  WITH (oids = false);  ALTER TABLE app.chats_question   OWNER TO mao;

id

question

answer_id

1

Сколько тебе лет

1

2

Лет то тебе сколько

1

3

Поговорим о возрасте

1

4

Трейлер фильма Матрица

2

5

Спокойной ночи

3

6

Пока мой друг

3

Продолжаем код в файле Chat_bot.py

# объявляем массив вопросов questions=[]   # загрузим вопросы и коды ответов cursor.execute('SELECT question, answer_id FROM app.chats_question;') records = cursor.fetchall()  # посчитаем количество вопросов transform=0  for row in records: # Если текст вопроса не пустой  if row[0]>"":    # Если в БД есть код ответа на вопрос   if row[1]>0:    phrases=row[0]        # разбираем вопрос на слова    words=phrases.split(' ')    phrase=""    for word in words:       # каждое слово из вопроса приводим в нормальную словоформу       word = morph.parse(word)[0].normal_form    # составляем фразу из нормализованных слов      phrase = phrase + word + " "               # Если длинна полученной фразы больше 0 добавляем ей в массив вопросов и массив кодов ответов    if (len(phrase)>0):     questions.append(phrase.strip())     answer_id.append(row[1])     transform=transform+1  # выведем на экран вопросы, ответы и коды ответов print (questions) print (answer) print (answer_id)  # Закроем подключение к PostgreSQL cursor.close() conn.close()

Векторизация и трансформация

# Векторизируем вопросы в огромную матрицу  # Перемножив фразы на слова из которых они состоят получим числовые значения  from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.decomposition import TruncatedSVD  vectorizer_q = TfidfVectorizer() vectorizer_q.fit(questions) matrix_big_q = vectorizer_q.transform(questions) print ("Размер матрицы: ") print (matrix_big_q.shape)  # Трансформируем матрицу вопросов в меньший размер для уменьшения объема данных # Трансформировать будем в 200 мерное пространство, если вопросов больше 200 # Размерность подбирается индивидуально в зависимости от базы вопросов, которая может содержать 1 млн. или 1к вопросов и 1 # Без трансформации большая матрицу будет приводить к потерям памяти и снижению производительности  if transform>200: transform=200  svd_q = TruncatedSVD(n_components=transform) svd_q.fit(matrix_big_q)  # получим трансформированную матрицу matrix_small_q = svd_q.transform(matrix_big_q)  print ("Коэффициент уменьшения матрицы: ") print ( svd_q.explained_variance_ratio_.sum()) 

Функция поиска ответа

# Тело программы поиска ответов  from sklearn.neighbors import BallTree from sklearn.base import BaseEstimator  def softmax(x):   #создание вероятностного распределения   proba = np.exp(-x)   return proba / sum(proba)  class NeighborSampler(BaseEstimator):   def __init__(self, k=5, temperature=10.0):     self.k=k     self.temperature = temperature   def fit(self, X, y):     self.tree_ = BallTree(X)     self.y_ = np.array(y)   def predict(self, X, random_state=None):     distances, indices = self.tree_.query(X, return_distance=True, k=self.k)     result = []     for distance, index in zip(distances, indices):       result.append(np.random.choice(index, p=softmax(distance * self.temperature)))     return self.y_[result]  from sklearn.pipeline import make_pipeline  ns_q = NeighborSampler()  # answer_id - код ответа в массиве, который получается при поиске ближайшего ответа ns_q.fit(matrix_small_q, answer_id)  pipe_q = make_pipeline(vectorizer_q, svd_q, ns_q)

Проверка из консоли

# код для проверки работы из консоли  print("Пишите ваш вопрос, слова exit или выход для выхода")  request=""  while request not in ['exit', 'выход']: # получим текст от ввода request=input()      # разберем фразу на слова  words= re.split('\W',request)  phrase=""  for word in words:     word = morph.parse(word)[0].normal_form  # морфируем слово вопроса в нормальную словоформу     # Нормализуем словоформу каждого слова и соберем обратно фразу     phrase = phrase + word + " "  # запустим функцию и получим код ответа  reply_id    = int(pipe_q.predict([phrase.strip()]))    # выведем текст ответа  print (answer[reply_id]) 

Запустим и проверим

python3 Chat_bot.py

Подключим Telegram

Установим библиотеку

# установим не самую последнюю версию для валидности дальнейшего кода #pip3 install PyTelegramBotAPI  pip3 install PyTelegramBotAPI==3.6.7

Откроем Telegram и обратимся к боту @BOTFATHER https://t.me/botfather

Все просто, зарегистрируем нового бота и получим token.

import telebot  telebot.apihelper.ENABLE_MIDDLEWARE = True  # Укажем token полученный при регистрации бота bot = telebot.TeleBot("9999999999:AABBCCDDEEFFGGQWERTYUIOPASDFGHJKLLK")  # Начнем обработку. Если пользователь запустил бота, ответим  @bot.message_handler(commands=['start']) def start_message(message): bot.send_message(message.from_user.id, " Здравствуйте. Я виртуальный бот Mao!")  # Если пользователь что-то написал, ответим @bot.message_handler(func=lambda message: True) def get_text_messages(message): request=message.text      # разобьём фразу на массив слов, используя split. '\W' - любой символ кроме буквы и цифры words= re.split('\W',request) phrase=""    # разберем фразу на слова, нормализуем каждое и соберем фразу for word in words: word = morph.parse(word)[0].normal_form       phrase = phrase + word + " "    # получим код ответа вызывая нашу функцию    reply_id    = int(pipe_q.predict([phrase.strip()]))      # отправим ответ bot.send_message(message.from_user.id, answer[reply_id])  # продублируем ответ пользователю с id=99999999 # bot.send_message(99999999, str(message.from_user.id) + "\n" + str(message.from_user.first_name) + " " + str(message.from_user.last_name) + " " +str(message.from_user.username) + "\n"+ str(request) + "\n"+ str(answer[reply_id]))      # выведем в консоль вопрос / ответа print("Запрос:", request, " \n\tНормализованный: ", phrase, " \n\t\tОтвет :", answer[reply_id])  # Запустим обработку событий бота bot.infinity_polling(none_stop=True, interval=1) 

В целом все готово. Вопросы в базу данных добавляются автоматически от службы тех. поддержки. Остаётся маркетологу в админ панели на YII назначать ответы вопросам. Раз в сутки cron перезапускает скрипт чат-бота, новые фразы поступают в работу.

Весь код чат бота

import csv import pymorphy2 import re   import psycopg2 conn = psycopg2.connect(dbname='energy', user='mao', password='daring', host='localhost') cursor = conn.cursor()  morph = pymorphy2.MorphAnalyzer(lang='ru')   answer_id=[]  answer = dict()  cursor.execute('SELECT id, answer FROM app.chats_answer;') records = cursor.fetchall() for row in records:  answer[row[0]]=row[1]  questions=[]   cursor.execute('SELECT question, answer_id FROM app.chats_question;') records = cursor.fetchall() transform=0  for row in records:  if row[0]>"":   if row[1]>0:    phrases=row[0]    words=phrases.split(' ')    phrase=""    for word in words:     word = morph.parse(word)[0].normal_form       phrase = phrase + word + " "    if (len(phrase)>0):     questions.append(phrase.strip())     answer_id.append(row[1])     transform=transform+1  #print (questions) #print (answer) #print (answer_id)  cursor.close() conn.close()  import sklearn from sklearn.feature_extraction.text import TfidfVectorizer  from sklearn.decomposition import TruncatedSVD  vectorizer_q = TfidfVectorizer() vectorizer_q.fit(questions) matrix_big_q = vectorizer_q.transform(questions) print ("Размер матрицы: ") print (matrix_big_q.shape)  if transform>200:  transform=200 print(transform) svd_q = TruncatedSVD(n_components=transform) svd_q.fit(matrix_big_q) matrix_small_q = svd_q.transform(matrix_big_q) print ("Коэффициент уменьшения матрицы: ") print ( svd_q.explained_variance_ratio_.sum())   # тело программы k=5, temperature=10.0 можно подбирать import numpy as np  from sklearn.neighbors import BallTree from sklearn.base import BaseEstimator  def softmax(x):   #создание вероятностного распределения   proba = np.exp(-x)   return proba / sum(proba)  class NeighborSampler(BaseEstimator):   def __init__(self, k=5, temperature=10.0):     self.k=k     self.temperature = temperature   def fit(self, X, y):     self.tree_ = BallTree(X)     self.y_ = np.array(y)   def predict(self, X, random_state=None):     distances, indices = self.tree_.query(X, return_distance=True, k=self.k)     result = []     for distance, index in zip(distances, indices):       result.append(np.random.choice(index, p=softmax(distance * self.temperature)))     return self.y_[result]  from sklearn.pipeline import make_pipeline  ns_q = NeighborSampler() ns_q.fit(matrix_small_q, answer_id)  pipe_q = make_pipeline(vectorizer_q, svd_q, ns_q)   import re import telebot telebot.apihelper.ENABLE_MIDDLEWARE = True bot = telebot.TeleBot("299999999:sdfgnreognrtgortgmrtgmrtgm")  @bot.message_handler(commands=['start']) def start_message(message): bot.send_message(message.from_user.id, " Здравствуйте. Я виртуальный помощник Mao?")  @bot.message_handler(func=lambda message: True) def get_text_messages(message): request=message.text words= re.split('\W',request) phrase="" for word in words: word = morph.parse(word)[0].normal_form   phrase = phrase + word + " " reply_id    = int(pipe_q.predict([phrase.strip()])) bot.send_message(message.from_user.id, answer[reply_id]) print("Запрос:", request, " \n\tНормализованный: ", phrase, " \n\t\tОтвет :", answer[reply_id])  bot.infinity_polling(none_stop=True, interval=1)   print("Пишите ваш вопрос, слова exit или выход для выхода") request="" while request not in ['exit', 'выход']:  request=input()  words= re.split('\W',request)  phrase=""  for word in words:   word = morph.parse(word)[0].normal_form     phrase = phrase + word + " "  reply_id    = int(pipe_q.predict([phrase.strip()]))  print (answer[reply_id])  

В теле программы есть переменные k=5 и temperature=10.0. Их можно менять, что будет влиять на поиск, делая его более мягким или более жестким.

P.S. Умышленно привожу весь код для практики. Теорию машинного обучения с картинками можно почитать, например, в другой статье.


ссылка на оригинал статьи https://habr.com/ru/articles/593065/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *