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