О чём все эти люди говорят, ChatGPT?

от автора

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

Промт для Kandinsky 2.1: "Кот в шоке, ему приходит очень много текстовых сообщений и писем, повсюду слова, он закопался в сообщения, вокруг хаос, листы с текстом, компьютер и чашка кофе", стиль: anime

Промт для Kandinsky 2.1: «Кот в шоке, ему приходит очень много текстовых сообщений и писем, повсюду слова, он закопался в сообщения, вокруг хаос, листы с текстом, компьютер и чашка кофе», стиль: anime

Всем привет! Я продуктовый аналитик компании Интерсвязь, и у меня, как и у многих, часто всплывает потребность в том, чтобы «разложить по полочкам» кучу разных текстов. Например:

  1. Я хочу знать, о чем вообще все отзывы в маркете про мой продукт.

  2. У меня есть много писем от клиентов на разные темы, и я хочу их систематизировать.

  3. Мне может понадобиться проанализировать старые обращения пользователей в техподдержку, которые не были размечены.

Для этого есть множество материалов про NLP, и чаще всего даже джуны, изучив только их часть, могут дать более-менее вменяемый результат. Кроме того, множество крупных компаний и вовсе имеют в штате специалистов по Data Science.

Но что делать, если:

  • Слабо понимаешь, что такое NLP;

  • Нет ресурсов, чтобы поднять у себя крутую модель с кучей параметров;

  • Роадмап NLP-специалистов в компании расписан на год вперед и для тебя нет места.

На помощь приходит OpenAI и их API. Открытое и доступное в РФ на момент написания статьи.

Про это и хотелось бы рассказать.

Для работы понадобятся:

  • API ключ из личного кабинета OpenAI;

  • Начальные навыки 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.


ссылка на оригинал статьи https://habr.com/ru/articles/727986/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *