Как в два счета разметить большие массивы текстов с помощью моделей от OpenAI

Всем привет! Я продуктовый аналитик компании Интерсвязь, и у меня, как и у многих, часто всплывает потребность в том, чтобы «разложить по полочкам» кучу разных текстов. Например:
Я хочу знать, о чем вообще все отзывы в маркете про мой продукт.
У меня есть много писем от клиентов на разные темы, и я хочу их систематизировать.
Мне может понадобиться проанализировать старые обращения пользователей в техподдержку, которые не были размечены.
Для этого есть множество материалов про NLP, и чаще всего даже джуны, изучив только их часть, могут дать более-менее вменяемый результат. Кроме того, множество крупных компаний и вовсе имеют в штате специалистов по Data Science.
Но что делать, если:
Слабо понимаешь, что такое NLP;
Нет ресурсов, чтобы поднять у себя крутую модель с кучей параметров;
Роадмап NLP-специалистов в компании расписан на год вперед и для тебя нет места.
На помощь приходит OpenAI и их API. Открытое и доступное в РФ на момент написания статьи.
Про это и хотелось бы рассказать.
Для работы понадобятся:
Начальные навыки python;
И, конечно же, надежный источник кофеина.
UPD: c мая 2023 года OpenAI установили ограничение на 3 запроса в минуту с бесплатных аккаунтов. Лучше обзавестись платным аккаунтом или посмотреть в сторону других моделей.
Готовим данные ??
Для работы понадобится среда для python. Будет удобнее, если вы умеете работать с jupyter или аналогами.
Понадобятся библиотеки от OpenAI и ещё несколько для работы с данными.
Установка библиотек:
pip install numpy==1.24.2 pandas==2.0.0 tqdm==4.65.0 openai==0.27.4 tiktoken==0.3.3 plotly==5.14.1
Импортируем библиотеки и готовим окружение.
import openai import tiktoken import numpy as np import pandas as pd from os import environ from tqdm import tqdm from openai.embeddings_utils import get_embedding openai.api_key = environ.get('OPENAI_TOKEN') tqdm.pandas()
Вместо environ.get('OPENAI_TOKEN') можно просто вставить строку с ключом, но использовать переменные окружения безопаснее.
Загрузим датасет с заранее предразмеченным вручную классом (столбец class). Это нужно, чтобы потом сравнить качество ручной (правильной) и автоматической разметки.
Ваш датасет, конечно, может не содержать столбец class.
Датасет из примера содержит 118 обращений в чат медицинской организации.
df = pd.read_csv('../data/appeals.csv') df

Заодно глянем на баланс предразмеченных классов.
df['class'].value_counts() # # Результаты анализов 36 # Изменение времени приема 25 # Пластическая хирургия 25 # Запись на прием 18 # Справки 14
Далее нам потребуются эмбеддинги сообщений. Это такое представление текста в виде вектора чисел, полученных от ml-модели. По идее, можно закодировать слова даже вручную, но качество числового представления сильно влияет на итоговый результат, и нынешние ml-модели неплохо определяют скрытые зависимости слов в тексте.
Выбираем, какая модель от OpenAI нам нужна для получения эмбеддингов, и решим, как они будут закодированы. Исходя из рекомендаций OpenAI лучше подходят text-embedding-ada-002 и cl100k_base.
embedding_model = "text-embedding-ada-002" embedding_encoding = "cl100k_base"
Далее мы почистим наш датасет, убрав слишком длинные сообщения. Этот пункт можно спокойно пропускать, если вам не жалко вашего баланса OpenAI.
max_tokens = 100 # Задаем максимальную длину токенов для сообщения encoding = tiktoken.get_encoding(embedding_encoding) # Рассчитываем длину сообщений в токенах df["n_tokens"] = df['message'].apply(lambda x: len(encoding.encode(x)))
Заодно узнаем, сколько мы потратим остатка на балансе.
print(f"Потратим: ${df['n_tokens'].sum() * 0.0004 / 1000}") # Потратим: $0.0011896
Убираем слишком длинные сообщения, чтобы не потратить слишком много токенов.
df = df[df["n_tokens"] <= max_tokens]
Получаем эмбеддинги!
Обычно это занимает около минуты на сотню сообщений.
Советую использовать progress_apply из tqdm, чтобы понять, как долго еще ждать.
df["embedding"] = df['message'].progress_apply(lambda x: get_embedding(x, engine=embedding_model)), # 100%|██████████| 118/118 [01:04<00:00, 1.82it/s]
Сохраним эмбеддинги отдельно.
matrix = np.vstack(df['embedding'].values) matrix.shape # (118, 1536)
Итак, у нас есть тексты и их числовые представления. Пора разбить эту кучку на кучки поменьше с помощью кластерного анализа.
Поиск кластеров с использованием K-means
from sklearn.cluster import KMeans n_clusters = 5 # Кол-во кластеров можно менять по усмотрению kmeans = KMeans(n_clusters=n_clusters, init="k-means++", random_state=42) kmeans.fit(matrix) labels = kmeans.labels_ df["сluster"] = labels
K-means — один из наиболее популярных способов разметить примеры на кластеры. На данном этапе количество кластеров можно подобрать визуально, зная примерно количество «тем», которое можно встретить в вашем датасете. А вообще подбор оптимального количества кластеров это тема, которую нужно рассматривать отдельно.
Полученные кластеры можно отобразить в 2D (и даже в 3D!). Это нужно, чтобы визуально понять, как сильно кластеры разделились. Для этого хорошо подходит t-SNE.
from sklearn.manifold import TSNE import matplotlib import matplotlib.pyplot as plt tsne = TSNE(n_components=2, perplexity=15, random_state=42, init="random", learning_rate=200) vis_dims2 = tsne.fit_transform(matrix) x = [x for x, y in vis_dims2] y = [y for x, y in vis_dims2] for category, color in enumerate(["purple", "green", "red", "blue", "yellow"]): xs = np.array(x)[df.сluster == category] ys = np.array(y)[df.сluster == category] plt.scatter(xs, ys, color=color, alpha=0.3) avg_x = xs.mean() avg_y = ys.mean() plt.scatter(avg_x, avg_y, marker="x", color=color, s=100) plt.title("Отображение кластеров в 2d используя t-SNE")

Как мы видим, четыре из пяти кластеров расположились относительно рядом. Это говорит о том, что они приблизительно схожи по тематике. Тогда как кластер снизу слева находится далеко от остальных.
Получаем названия для кластеров
Для того, чтобы примерно понять суть кластера и подобрать название, мы возьмём по пять случайных сообщений в каждом кластере и попросим модель gpt-3.5-turbo из ChatGPT описать, что у них общего, ради хайпового заголовка потому что сейчас это одна из самых качественных моделей для суммаризации от OpenAI. В то же время и модель text-davinci-003 отлично подойдёт, но будет стоить в 10 раз дороже.
Стоит обратить внимание на то, что стоимость генерации выйдет дороже получения эмбеддингов. Стоимость можно ограничивать:
Контролируя размер переменной
promt. Это затравка, которая передаётся в модель для получения ответа на неё.Контролируя размер получаемого ответа, изменяя параметр
max_tokens.
msg_per_cluster = 5 # Количество сообщений на кластер for i in range(n_clusters): joined_messages = "\n".join( df[df['сluster'] == i] .['message'] .sample(msg_per_cluster, random_state=42) .values ) promt = f'Что общего у этих обращений?\n\nОбращения:\n"""\n{joined_messages}\n"""\n\nТема:' response = openai.ChatCompletion.create( model="gpt-3.5-turbo", messages=[ { "role": "user", "content": promt } ], temperature=0, max_tokens=128, # Этот параметр можно изменять для более подробного или короткого описания top_p=1, frequency_penalty=0, presence_penalty=0, ) print(f"Тема кластера №{i}: ", response['choices'][0]['message']['content'].replace("\n", ""), '\n') print(joined_messages) print('\n\n') # Тема кластера №0: Изменение времени приема у врача. # Как отменить изменение времени приема? # Есть ли ограничения по количеству раз, когда можно изменить время приема? # Могу ли я изменить время приема через интернет? # Какие данные нужно предоставить для изменения времени приема? # Могу ли я выбрать время приема врача? # Тема кластера №1: Медицинские справки и их получение. # Какие медицинские справки нужны для трудоустройства? # Какие медицинские справки нужны для получения водительского удостоверения? # Как получить медицинскую справку? # Какие медицинские справки нужны для выезда за границу? # Могу ли я получить медицинскую справку по почте или электронной почте? # Тема кластера №2: Пластическая хирургия. # Добрый день, я хочу сделать операцию по подтяжке лица. Как долго будет идти восстановление после операции? # Добрый день, я хотел бы узнать о возможностях пластической операции по удалению рубцов. # Здравствуйте, я хотел бы узнать, какую процедуру можно сделать для коррекции формы бровей? # Добрый день, мне не нравится форма моего живота. Какие есть варианты пластической коррекции? # Здравствуйте, я хочу сделать операцию по изменению формы ушей. Какие ограничения будут после операции? # Тема кластера №3: Результаты анализов. # Какие услуги могут быть оказаны на основе результатов анализов? # Как мне получить результаты анализов? # Какие данные нужно предоставить для получения результатов анализов? # Как понять, что означают результаты анализов? # Могу ли я получить результаты анализов у лечащего врача? # Тема кластера №4: Запись на прием к врачу. # Как мне записаться на прием к врачу? # Какие документы нужны для записи на прием? # Как отменить запись на прием? # Какие данные нужно указать при онлайн-записи? # Могу ли я записаться на прием через интернет?
Кластер №2 как раз тот, который «отдалялся» на графике с t-SNE. И он действительно тематически далёк от других тем датасета.
Проверочная работа
У нас теперь есть описание кластеров от gpt-3.5-turbo. Эти названия можно сопоставить с тем, как мы разметили сообщения в начале.
df['cluster_name'] = df['сluster'].replace( { 0: 'Изменение времени приема', 1: 'Справки', 2: 'Пластическая хирургия', 3: 'Результаты анализов', 4: 'Запись на прием' })
Посмотрим баланс классов теперь:
df['cluster_name'].value_counts() # Результаты анализов 36 # Пластическая хирургия 25 # Изменение времени приема 24 # Запись на прием 19 # Справки 14
Он немного изменился. Выведем ошибки.
df[df['class'] != df['cluster_name']]

Их всего 4 на 118 сообщений. И на самом деле их сложно назвать ошибками, т.к. эти сообщения действительно можно отнести к двум кластерам сразу.
В итоге за $0.01 (за эмбеддинги и суммаризацию текстов) мы разметили больше сотни сообщений быстрее, чем за пять минут!
Даже если учесть, что в части данных мы точно ошибемся, количество ошибок будет вряд ли больше, чем при человеческой разметке.
Однако стоит учитывать, что никакая модель без дообучения не будет знать нюансов и бизнес-процессов компании. Так что лучше использовать эти инструменты для поверхностной аналитики, а не как инструмент для разбора «изнутри».
За референс взят эксперимент openai. Весь код и датасет выложен в GitHub.