Пошаговое руководство написания чат-бота на языке 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 |
Я тоже хочу спать |
Структура таблицы вопросов 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. Умышленно привожу весь код для практики. Теорию машинного обучения с картинками можно почитать, например, в другой статье.