Пишем чат-бот на 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:
    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

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

Продолжаем код в файле 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/post/593065/


Интересные статьи

Интересные статьи

Привет, Хабр! У нас возможен предзаказ долгожданного второго издания книги "Простой Python". Перевод первого издания вышел в 2016 году и по сей день остается в числе бестселл...
Недавно мы пригласили в эфир "Цинкового прода" Алексея Лесовского из компании Data Egret. Разговор получился интересный и познавательный, поэтому предлагаю вашему вниманию расшифровку э...
TL; DR: JSONB может значительно упростить разработку схемы БД без ущерба производительности в запросах. Введение Приведем классический пример, наверное, одного из старейших вариантов использова...
Для всех хабравчан, у которых возникло ощущение дежавю: Написать этот пост меня побудили статья "Введение в Python" и комментарии к ней. К сожалению, качество этого "введения" кхм… не будем о гру...
Практически все коммерческие интернет-ресурсы создаются на уникальных платформах соответствующего типа. Среди них наибольшее распространение получил Битрикс24.