Масштабируем ML и NLP в БД с помощью PL/Python
Это третья статье из серии статей, посвященных теме Greenplum и Data Science. В этой статье мы расскажем о том, как объединить возможности Greenplum и богатую экосистему Python для того, чтобы в разы ускорить процесс разработки моделей ML и NLP.
Сочетание возможностей Greenplum с богатой экосистемой Python значительно ускоряет процесс разработки моделей ML и NLP.
На рисунке ниже проиллюстрирована потребность в смене парадигмы при обучении и выводе ML-моделей на уровень Big Data.
- In-client ML подразумевает перемещение данных из источника данных в вычислительную среду, но этот подход не работает при больших объемах данных (например, петабайтах данных).
- In-database ML переносит вычисления в источник данных, исключая этап перемещения данных, что дает возможность увеличения масштабируемости и производительности.
Установка и подключение
- Установите пакеты
pip install ipython-sql pandas numpy sqlalchemy plotly-express sql_magic pgspecial
- Импортируйте пакеты
import pandas as pd import numpy as np import os import sys import plotly_express as px # For DB Connection from sqlalchemy import create_engine import psycopg2 import pandas.io.sql as psql import sql_magic
3. Подключение к базе данных
Существует несколько способов установить соединение с Greenplum. В блокноте Jupyter можно использовать «магическую» SQL - команду. Это позволит выполнить SQL-запросы в ячейке Jupyter. Ссылка на документацию:: https://pypi.org/project/ipython-sql/ installation on the client:
pip install ipython-sql
Для использования блокнота достаточно установить следующее соединение:
%load_ext _ sql %sql postgresql://<user>:<password>@<IP_address>:<port>/<database_name>
Затем выполните следующие команды SQL
%%sql SELECT version ();
Вы также можете использовать коннектор psycopg2 или pyodbc (существуют и другие варианты, например, JDBC)
Обзор набора данных
Набор данных состоит из 1967 финансовых новостей, написанных на английском языке, которые могут быть либо позитивными, либо негативными.
Название полей:
- docid: идентификатор новостей
- original_news: текст новостей
- label: тональность новости - позитивная новость или негативная
%%sql SELECT * FROM ds_demo.sentiment_news LIMIT 5;
%sql SELECT count(*) FROM ds_demo.sentiment_news;
NLP - анализ тональности новостей с помощью PL/Python
Анализ тональности направлен на определение «настроения», необходимого для принятия обоснованных решений; он широко используется в финансовых компаниях для прогнозирования того, могут ли данные новости положительно или отрицательно повлиять на будущую цену акций.
Анализ настроений - это задача классификации текстов. В этом разделе для преобразования текстов в числовое представление мы используем технику Bag of Words. Преобразование текстов может быть использовано для обучения модели логистической регрессии..
- Распределение классов тональности в нашем наборе данных:
%%read_sql df_count_sentiment SELECT label, count(*) AS number_of_news FROM ds_demo.sentiment_news GROUP BY 1 ORDER BY 2 ASC
color_discrete_map = {'negative': 'rgb(255,0,0)',
'positive': 'rgb(0,255,0)' }
px.pie(df_count_sentiment,
values = 'number_of_news',
names = 'label',
color = 'label',
color_discrete_map=color_discrete_map)
На графике видно, что наш набор данных несбалансирован: положительных новостей в два раза больше, чем отрицательных. Это может повлиять на производительность нашей модели.
- Количество символов
%%read_sql df_max_length SELECT max(length(original_news)) FROM ds_demo.sentiment_news;
Самая «длинная» новость в нашем наборе данных содержит чуть меньше 300 символов, что означает, что тональность текста может быть определена на основе достаточно «короткого» текста.
- Количество символов по классам тональности:
%%read_sql df_len_news
SELECT original_news,
label,
length(original_news) * 1.0 / max(length(original_news))
OVER (partition by NULL) AS ratio_chars
FROM ds_demo.sentiment_news
px.box(df_len_news,
y="ratio_chars",
x ='label',
color='label',
title = 'Number of characters distribution by sentiment class',
color_discrete_map=color_discrete_map)
- Количество слов по классам тональности:
%%read_sql df_len_words
SELECT original_news, label,
ARRAY_LENGTH(STRING_TO_ARRAY(original_news, ' '), 1)
AS number_of_words
FROM ds_demo.sentiment_news
px.box(df_len_words,
y="number_of_words",
x ='label',
color='label',
title = 'Number of words distribution by sentiment class',
color_discrete_map=color_discrete_map)
Позитивные новости, как правило, содержат больше слов и символов, чем негативные новости, но все же мы не можем основывать наш анализ данных на длине новостей.
Предварительная обработка данных - очистка текста
Под очисткой текста понимается удаление или преобразование определенных частей текста таким образом, чтобы текст стал более понятным для моделей NLP, изучающих этот текст. Это часто позволяет моделям NLP работать быстрее за счет уменьшения «шума» в тексте.
Для этого мы создадим PL/Python и выполним достаточно простые действия по предварительной обработке данных:
- Преобразуем текст в строки.
- Затем удалим все специальные символы и URL-адреса
- И наконец, удалим все пробелы.
%%sql
DROP FUNCTION IF EXISTS text_prepare(text);
CREATE OR REPLACE FUNCTION text_prepare( content text)
RETURNS text
AS $$
import re
text = content
replace_by_space_re = re.compile('[/(){}\[\]\|@,;]')
bad_symbols_re = re.compile('[^0-9a-z #+_]')
links_re = re.compile('(www|http)\S+')
text = text.lower() # lowercase text
text = re.sub(replace_by_space_re," ",text)
text = re.sub(bad_symbols_re, "",text)
text = re.sub(links_re, "",text)
text = re.sub(' +', ' ', text)
return text.strip()
$$ LANGUAGE plpython3u
;
Сохраним обработанные новости в новом столбце под названием clean_text, который будет использоваться для обучения моделей.
%%sql ALTER TABLE ds_demo.sentiment_news ADD COLUMN cleaned_text text; UPDATE ds_demo.sentiment_news SET cleaned_text = text_prepare(original_news::text);
Краткий обзор новостей, прошедших очистку:
%sql SELECT * FROM ds_demo.sentiment_news LIMIT 2;
Обучение модели анализа тональности новостей с помощью PL/Python
Обучение модели NLP для автоматического прогнозирования тональности финансовых новостей может помочь трейдерам и инвесторам в разработке торговых стратегий.
Анализ настроений - это задача классификации текстов. В данном случае для преобразования текстов в числовое представление, которое может быть использовано для обучения модели логистической регрессии, мы будем использовать технику Bag of Words.
Подготовка модели - логистическая регрессия по частоте терминов
Давайте напишем функцию PL/Python, которую можно вызвать как и любую другую функцию SQL. Интеграция данной функции не требует особых усилий, поскольку в Python есть бесконечное количество библиотек для ML.
Более того, помимо полной поддержки Python, PL/Python также предоставляет достаточно удобные функции для выполнения любого параметризованного запроса. Таким образом, выполнение алгоритмов ML может стать вопросом всего лишь пары строк.
%%sql
DROP FUNCTION IF EXISTS train_sentiment_news(text[],text[]);
DROP TYPE IF EXISTS news_type;
-- Create a new type for our PL/Python function output
CREATE type news_type AS (content text, label text, prediction text);
-- PL/Python function using Python3.9
CREATE FUNCTION train_sentiment_news( cleaned_text text[], label text[])
RETURNS SETOF news_type
AS $$
import pandas as pd
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
X = cleaned_text
y = label
df= pd.DataFrame()
df['content'] = X
df['label'] = y
X_train, X_test, y_train, y_test = train_test_split(df['content'], df['label'], test_size=0.2, random_state=42)
vectorizer = CountVectorizer(min_df=4, stop_words='english')
X_train = vectorizer.fit_transform(X_train)
X_test = vectorizer.transform(X_test)
# LOGISTIC REGRESSION
logreg = LogisticRegression()
# TRAIN
logreg.fit(X_train, y_train)
X = vectorizer.transform(df['content'])
# PREDICTIONS ON FULL DATASET
lr_prediction = logreg.predict(X)
df['prediction'] = lr_prediction
return [{'content': str(row['content']),
'label': str(row['label']),
'prediction': str(row['prediction'])}
for index, row in df.iterrows()]
$$ LANGUAGE plpython3u;
Как видите, PL/Python – это очень просто.
- Сначала мы импортируем необходимые нам пакеты; мы используем библиотеки pandas и scikit-learn.
- Нам нужно загрузить входные данные (новости и тональность) в датафрейм и преобразовать числовые переменные в числовой тип с помощью Bag of Words / CountVectorizer
- Затем мы выбираем логистическую регрессию и обучаем ее на обучающем наборе (80% нашего набора данных).
- Наконец, мы возвращаем прогноз по всему набору данных в виде списка типов news_type.
В последней строке указываем язык расширения: в данном случае мы используем Python3, поэтому расширение называется plpython3u. Если Вы хотите использовать Python2, используйте язык расширения plpythonu.
Greenplum также предоставляет PL/Container, который запускает PL/Container в Docker.
Отображение прогнозов
Мы можем проверить прогнозы, составленные нашей моделью.
Функция train_sentiment_news() использует два массива, поэтому нам нужно применить функцию ARRAY_AGG() для того, чтобы объединить все записи столбцов clean_text и labels в два массива.
Кроме того, поскольку функция train_sentiment_news() возвращает набор new_type, нам необходимо разложить каждую запись результатов на столбцы с помощью CTE (Common Table Expression).
%%read_sql df_preds
WITH cte_data_array_agg AS (
SELECT ARRAY_AGG(cleaned_text) AS contents,
ARRAY_AGG(label) AS labels
FROM ds_demo.sentiment_news
),
cte_predictions AS (
SELECT train_sentiment_news(t.contents, t.labels)
FROM cte_data_array_agg
)
SELECT (train_sentiment_news::news_type).* FROM cte_predictions;
Оценка модели
Теперь предлагаю оценить нашу модель по таким параметрам, как точность, оценке F1-score, правильность и отзыв
%%sql
DROP FUNCTION IF EXISTS metrics_report(text[],text[]);
DROP TYPE IF EXISTS prediction_type;
CREATE type prediction_type AS (accuracy float, f1_score float, precision float, recall float);
CREATE OR REPLACE FUNCTION metrics_report(label text[], prediction text[])
RETURNS prediction_type
AS $$
from sklearn.metrics import precision_recall_fscore_support as score
from sklearn.metrics import accuracy_score
import numpy as np
y_true = label
y_pred = prediction
accuracy = accuracy_score(y_true, y_pred)
precision, recall, f1_score, support = score(y_true, y_pred)
precision = float(np.mean(precision))
f1_score = float(np.mean(f1_score))
recall = float(np.mean(recall))
accuracy = float(accuracy)
return {'accuracy': accuracy,
'f1_score': f1_score,
'precision': precision,
'recall': recall
}
$$ LANGUAGE plpython3u
;
Для объединения столбцов и разложения результатов композитного типа с помощью CTE мы применим функцию ARRAY_AGG().
%%read_sql df_preds
WITH
cte_data_array_agg AS (
SELECT ARRAY_AGG(cleaned_text) AS contents, ARRAY_AGG(label) AS labels
FROM ds_demo.sentiment_news
),
cte_predictions AS (
SELECT train_sentiment_news(t.contents, t.labels)
FROM cte_data_array_agg
),
cte_label_pred AS (
select (train_sentiment_news::news_type).*
FROM cte_predictions
),
cte_metrics_report AS (
SELECT metrics_report(array_agg(label), array_agg(prediction))
FROM cte_label_pred
)
SELECT (metrics_report::prediction_type).* FROM cte_metrics_report
Точность модели составляет 92,47%, f1-score, отзыв и правильность – чуть меньше.
Развертывание модели - хранение модели
Нет особого смысла в создании модели, с который Вы не планируете работать дальше.. Поэтому нам нужно сохранить ее в виде двоичных файлов, используя тип данных «bytea».
Сохранение модели в двоичном формате в таблице SQL
%%sql
DROP FUNCTION IF EXISTS save_nlp_models(text[],text[]);
DROP TYPE IF EXISTS model_type CASCADE;
CREATE TYPE model_type as (model_logreg bytea, model_bow bytea);
CREATE FUNCTION save_nlp_models(cleaned_text text[], label text[])
RETURNS model_type
AS $$
import pandas as pd
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
import pickle
X = cleaned_text
y = label
df= pd.DataFrame()
df['content'] = X
df['label'] = y
X_train, X_test, y_train, y_test = train_test_split(df['content'],
df['label'],
test_size=0.2,
random_state=42)
vectorizer = CountVectorizer(min_df=4, stop_words='english')
X_train = vectorizer.fit_transform(X_train)
X_test = vectorizer.transform(X_test)
# LOGISTIC REGRESSION
logreg = LogisticRegression()
logreg.fit(X_train, y_train)
X = vectorizer.transform(df['content'])
lr_prediction = logreg.predict(X)
df['prediction'] = lr_prediction
# Save Logistic Regression model
model_logreg = pickle.dumps(logreg)
# Save CountVectorizer
model_countvectorizer = pickle.dumps(vectorizer)
return {'model_logreg': model_logreg
,'model_bow': model_countvectorizer}
$$ LANGUAGE plpython3u
;
Для сохранения таблицы создадим таблицу ds_demo.saved_models
%%sql
DROP TABLE IF EXISTS ds_demo.saved_models;
CREATE TABLE ds_demo.saved_models (model_logreg bytea,
model_bow bytea,
model_name text);
В данном случае наша таблица содержит только model_name и два массива (одно для Logistic Regression, а другое для Bag of Words), которые и являются сериализованной моделью. Обратите внимание на то, что это тот же тип данных, что и тот, который возвращают наши модели.
Scikit-learn: получив таблицу, мы можем легко вставить в нее новую запись с помощью модели:
%%sql
WITH cte_models AS (
SELECT save_nlp_models(t.contents, t.labels)
FROM (
SELECT ARRAY_AGG(cleaned_text) AS contents,
ARRAY_AGG(label) AS labels
FROM ds_demo.sentiment_news) t
),
cte_casted_model AS (
SELECT (save_nlp_models::model_type).*
FROM cte_models)
INSERT INTO ds_demo.saved_models
SELECT model_logreg, model_bow, 'nlp_sentiment_analysis_bow_logreg' AS model_name FROM cte_casted_model;
Отображение двоичных файлов
%%sql SELECT model_name, model_logreg::text, model_bow::text FROM ds_demo.saved_models;
Отображение информации о модели
До сих пор мы могли только создавать и хранить модели. Но получать их непосредственно из базы данных не так удобно, как того хотелось бы.
Поэтому для того, чтобы отобразить полезную информацию о нашей модели, мы должны вернуться к Python.
%%sql
DROP FUNCTION get_model_info(text,text,text);
CREATE OR replace FUNCTION get_model_info(model_table text, model_column text, model_name text)
RETURNS text
AS $$
from pandas import DataFrame
import pickle
rv = plpy.execute('SELECT %s FROM %s WHERE model_name = %s;' % (plpy.quote_ident(model_column), model_table, "'"+model_name+"'"))
model = pickle.loads(rv[0][model_column])
return str(model.get_params())
$$ LANGUAGE plpython3u;
Начнем с самого начала: мы снова передаем таблицу, содержащую модели, и столбец, в котором хранятся бинарные данные. Функция pickle.load() считывает результат.
(Здесь показано, как именно результаты запроса plpython plpy.execute загружаются в Python).
После загрузки модели мы получаем model.get_params(), где хранятся параметры логистической регрессии.
Это всего лишь пример вывода конкретной характеристики модели. Вы можете создать аналогичные функции для возврата других характеристик или даже всех возможных характеристик.
Давайте посмотрим на результат:
%%sql
select get_model_info('ds_demo.saved_models','model_logreg','nlp_sentiment_analysis_bow_logreg');
Вывод модели - прогнозирование новых данных
Теперь, когда у нас есть модель, давайте используем ее в целях прогнозирования! Вызов модели достаточно прост и может быть выполнен на SQL с использованием PL/Python.
%%sql
DROP FUNCTION IF EXISTS predict_sentiment(text, bytea, bytea);
CREATE FUNCTION predict_sentiment(cleaned_text text, model_logreg bytea, model_bow bytea)
RETURNS text
AS $$
import pickle
logreg = model_logreg
vectorizer = model_bow
texts = cleaned_text
# Save Logistic Regression model
model_logreg_bytes = pickle.loads(logreg)
# Save CountVectorizer
model_countvectorizer = pickle.loads(vectorizer)
return list(model_logreg_bytes.predict(model_countvectorizer.transform([texts])))[0]
$$ LANGUAGE plpython3u
;
По сравнению с предыдущей функцией, мы добавляем один входной параметр (cleaned_text), передавая на вход фрагмент финансовой новости, для которой мы хотим получить тональность.
%%sql
SELECT content, predict_sentiment(text_prepare(t.content), b.model_logreg, b.model_bow)
FROM
ds_demo.financial_news t,
ds_demo.saved_models b
LIMIT 5;
Теперь мы можем обрабатывать большие массивы данных и показывать распределение положительных/отрицательных новостей.
%%sql
SELECT predict_sentiment, count(*)
FROM (
SELECT content, predict_sentiment(t.content, b.model_logreg, b.model_bow)
FROM
ds_demo.financial_news t,
ds_demo.saved_models b
LIMIT 10000
) results
GROUP BY 1;
Заключение
В этой статье мы показали Вам, как можно обучать модели NLP и ML, не выходя из Greenplum, обладающего беспрецедентными возможностями аналитики данных, необходимых для решения сложнейших задач в области Data Science.
Использование возможностей ML в Greenplum, а также в PL/Python и PL/R позволяет специалистам по исследованию данных использовать обширную экосистему библиотек ML для анализа Big Data.





















