Мы собираемся представить, что у нас есть реальный продукт, который нужно улучшить. Мы изучим набор данных и опробуем различные модели, такие как логистическая регрессия, рекуррентные нейронные сети и преобразователи, проверив, насколько они точны, как они собираются улучшить продукт, насколько быстро они работают и легко ли их отлаживать. и масштабировать.
Вы можете прочитать полный код тематического исследования на GitHub и просмотреть блокнот анализа с интерактивными диаграммами в Jupyter Notebook Viewer .
Взволнованный? Давайте займемся этим!
Представьте, что у нас есть сайт электронной коммерции. На этом сайте продавец может загрузить описания товаров, которые он хочет продать. Им также приходится выбирать категории предметов вручную, что может замедлить их работу.
Наша задача — автоматизировать выбор категорий на основе описания товара. Однако ошибочный автоматизированный выбор хуже, чем отсутствие автоматизации, поскольку ошибка может остаться незамеченной, что может привести к потерям в продажах. Поэтому мы можем отказаться от установки автоматической метки, если не уверены.
Для этого тематического исследования мы будем использовать
Ниже мы рассмотрим несколько модельных архитектур, и всегда полезно решить, как выбрать лучший вариант, прежде чем мы начнем. Как эта модель повлияет на наш продукт? …наша инфраструктура?
Очевидно, у нас будет метрика технического качества, позволяющая сравнивать различные модели в автономном режиме. В данном случае у нас есть задача классификации нескольких классов, поэтому давайте использовать сбалансированную оценку точности , которая хорошо обрабатывает несбалансированные метки.
Конечно, типичным финальным этапом тестирования кандидата является AB-тестирование — онлайн-этап, который дает лучшее представление о том, как изменения повлияют на клиентов. Обычно AB-тестирование занимает больше времени, чем оффлайн-тестирование, поэтому тестируются только лучшие кандидаты с оффлайн-этапа. Это тематическое исследование, и у нас нет реальных пользователей, поэтому мы не будем рассматривать AB-тестирование.
Что еще нам следует учитывать, прежде чем продвигать кандидата на AB-тестирование? О чем мы можем подумать на этапе автономного тестирования, чтобы сэкономить время на онлайн-тестирование и убедиться, что мы действительно тестируем наилучшее возможное решение?
Сбалансированная точность — это здорово, но эта оценка не отвечает на вопрос: «Как именно модель повлияет на продукт?». Чтобы найти более ориентированную на продукт оценку, мы должны понять, как мы собираемся использовать модель.
В наших условиях ошибиться хуже, чем не ответить, потому что продавцу придется заметить ошибку и изменить категорию вручную. Незамеченная ошибка снизит продажи и ухудшит пользовательский опыт продавца, мы рискуем потерять клиентов.
Чтобы избежать этого, мы выберем пороговые значения для оценки модели так, чтобы позволить себе только 1% ошибок. Тогда метрика, ориентированная на продукт, может быть установлена следующим образом:
Какой процент элементов мы можем классифицировать автоматически, если наша устойчивость к ошибкам составляет всего 1%?
Ниже мы будем называть это Automatic categorisation percentage
при выборе лучшей модели. Полный код выбора порога можно найти здесь .
Сколько времени модели требуется для обработки одного запроса?
Это примерно позволит нам сравнить, сколько еще ресурсов нам придется поддерживать, чтобы сервис мог справиться с нагрузкой задач, если одна модель будет выбрана вместо другой.
Когда наш продукт будет расти, насколько легко будет управлять ростом, используя данную архитектуру?
Под ростом мы можем понимать:
Придется ли нам переосмыслить выбор модели, чтобы справиться с ростом, или будет достаточно простого переобучения?
Насколько легко будет отлаживать ошибки модели во время обучения и после развертывания?
Размер модели имеет значение, если:
Позже мы увидим, что оба приведенных выше пункта не имеют значения, но кратко рассмотреть их все же стоит.
С чем мы работаем? Давайте посмотрим на данные и посмотрим, нужно ли их очищать!
Набор данных содержит 2 столбца: описание элемента и категория, всего 50,5 тыс. строк.
file_name = "ecommerceDataset.csv" data = pd.read_csv(file_name, header=None) data.columns = ["category", "description"] print("Rows, cols:", data.shape) # >>> Rows, cols: (50425, 2)
Каждому товару присвоена 1 из 4 доступных категорий: Household
», Books
, Electronics
или Clothing & Accessories
. Вот 1 пример описания товара для каждой категории:
Бытовой SPK Домашний декор Глиняная настенная подвеска ручной работы «Лицо» (разноцветная, В35хШ12см) Сделайте свой дом красивее с помощью этой настенной подвески ручной работы «Терракотовая индийская маска для лица», никогда прежде вы не сможете поймать эту вещь ручной работы на рынке. Вы можете добавить это в свою гостиную / входной вестибюль.
Книги BEGF101/FEG1-Базовый курс на английском языке-1 (Neeraj Publications, издание 2018 г.) BEGF101/FEG1-Базовый курс на английском языке-1
Одежда и аксессуары Женский джинсовый комбинезон Broadstar Получите пропуск на полный доступ в комбинезонах Broadstar от Broadstar. В этих комбинезонах из джинсовой ткани вам будет комфортно. Сочетайте их с верхом белого или черного цвета, чтобы завершить повседневный образ.
Electronics Caprigo Heavy Duty — кронштейн для потолочного крепления проектора премиум-класса высотой 2 фута (регулируемый — белый — вес 15 кг)
В наборе данных есть только одно пустое значение, которое мы собираемся удалить.
print(data.info()) # <class 'pandas.core.frame.DataFrame'> # RangeIndex: 50425 entries, 0 to 50424 # Data columns (total 2 columns): # # Column Non-Null Count Dtype # --- ------ -------------- ----- # 0 category 50425 non-null object # 1 description 50424 non-null object # dtypes: object(2) # memory usage: 788.0+ KB data.dropna(inplace=True)
Однако дублированных описаний довольно много. К счастью, все дубликаты относятся к одной категории, поэтому мы можем их безопасно удалить.
repeated_messages = data \ .groupby("description", as_index=False) \ .agg( n_repeats=("category", "count"), n_unique_categories=("category", lambda x: len(np.unique(x))) ) repeated_messages = repeated_messages[repeated_messages["n_repeats"] > 1] print(f"Count of repeated messages (unique): {repeated_messages.shape[0]}") print(f"Total number: {repeated_messages['n_repeats'].sum()} out of {data.shape[0]}") # >>> Count of repeated messages (unique): 13979 # >>> Total number: 36601 out of 50424
После удаления дубликатов у нас осталось 55% исходного набора данных. Набор данных хорошо сбалансирован.
data.drop_duplicates(inplace=True) print(f"New dataset size: {data.shape}") print(data["category"].value_counts()) # New dataset size: (27802, 2) # Household 10564 # Books 6256 # Clothing & Accessories 5674 # Electronics 5308 # Name: category, dtype: int64
Обратите внимание, что согласно описанию набора данных,
Набор данных был взят с индийской платформы электронной коммерции.
Описания не обязательно написаны на английском языке. Некоторые из них написаны на хинди или других языках с использованием символов, отличных от ASCII, или транслитерированы латинским алфавитом, или используют смесь языков. Примеры из категории Books
:
यू जी सी – नेट जूनियर रिसर्च फैलोशिप एवं सहायक प्रोफेसर योग्यता …
Prarambhik Bhartiy Itihas
History of NORTH INDIA/வட இந்திய வரலாறு/ …
Чтобы оценить наличие неанглийских слов в описаниях, посчитаем 2 балла:
Word2Vec-300
, обученном на корпусе английского языка.
Используя ASCII-оценку, мы узнаем, что только 2,3% описаний состоят из более чем 1% символов, отличных от ASCII.
def get_ascii_score(description): total_sym_cnt = 0 ascii_sym_cnt = 0 for sym in description: total_sym_cnt += 1 if sym.isascii(): ascii_sym_cnt += 1 return ascii_sym_cnt / total_sym_cnt data["ascii_score"] = data["description"].apply(get_ascii_score) data[data["ascii_score"] < 0.99].shape[0] / data.shape[0] # >>> 0.023
Оценка допустимых английских слов показывает, что только 1,5% описаний содержат менее 70% допустимых английских слов среди слов ASCII.
w2v_eng = gensim.models.KeyedVectors.load_word2vec_format(w2v_path, binary=True) def get_valid_eng_score(description): description = re.sub("[^az \t]+", " ", description.lower()) total_word_cnt = 0 eng_word_cnt = 0 for word in description.split(): total_word_cnt += 1 if word.lower() in w2v_eng: eng_word_cnt += 1 return eng_word_cnt / total_word_cnt data["eng_score"] = data["description"].apply(get_valid_eng_score) data[data["eng_score"] < 0.7].shape[0] / data.shape[0] # >>> 0.015
Поэтому большинство описаний (около 96%) на английском или преимущественно на английском языке. Мы можем удалить все остальные описания, но вместо этого давайте оставим их как есть и посмотрим, как каждая модель с ними справится.
Давайте разделим наш набор данных на 3 группы:
Train 70% - за обучение моделей (19к сообщений)
Тест 15% - на выбор параметров и порогов (4,1 тыс. сообщений)
Eval 15% - за выбор финальной модели (4,1к сообщений)
from sklearn.model_selection import train_test_split data_train, data_test = train_test_split(data, test_size=0.3) data_test, data_eval = train_test_split(data_test, test_size=0.5) data_train.shape, data_test.shape, data_eval.shape # >>> ((19461, 3), (4170, 3), (4171, 3))
Полезно сначала сделать что-нибудь простое и тривиальное, чтобы получить хорошую основу. В качестве основы давайте создадим структуру «мешок слов» на основе набора данных поезда.
Давайте также ограничим размер словаря 100 словами.
count_vectorizer = CountVectorizer(max_features=100, stop_words="english") x_train_baseline = count_vectorizer.fit_transform(data_train["description"]) y_train_baseline = data_train["category"] x_test_baseline = count_vectorizer.transform(data_test["description"]) y_test_baseline = data_test["category"] x_train_baseline = x_train_baseline.toarray() x_test_baseline = x_test_baseline.toarray()
Я планирую использовать логистическую регрессию в качестве модели, поэтому перед обучением мне нужно нормализовать функции счетчика.
ss = StandardScaler() x_train_baseline = ss.fit_transform(x_train_baseline) x_test_baseline = ss.transform(x_test_baseline) lr = LogisticRegression() lr.fit(x_train_baseline, y_train_baseline) balanced_accuracy_score(y_test_baseline, lr.predict(x_test_baseline)) # >>> 0.752
Многоклассовая логистическая регрессия показала сбалансированную точность 75,2%. Это отличная база!
Хотя общее качество классификации невелико, модель все же может дать нам некоторую информацию. Давайте посмотрим на матрицу путаницы, нормализованную по количеству предсказанных меток. Ось X обозначает прогнозируемую категорию, а ось Y — реальную категорию. Глядя на каждый столбец, мы можем увидеть распределение реальных категорий, когда определенная категория была предсказана.
Например, Electronics
часто путают с Household
. Но даже эта простая модель может довольно точно отобразить Clothing & Accessories
.
Вот важные функции при прогнозировании категории Clothing & Accessories
:
Топ-6 самых популярных слов в пользу и против категории Clothing & Accessories
:
women 1.49 book -2.03 men 0.93 table -1.47 cotton 0.92 author -1.11 wear 0.69 books -1.10 fit 0.40 led -0.90 stainless 0.36 cable -0.85
Теперь рассмотрим более продвинутые модели, созданные специально для работы с последовательностями — рекуррентные нейронные сети . GRU и LSTM — это общие расширенные уровни для борьбы с взрывными градиентами, возникающими в простых RNN.
Мы будем использовать библиотеку pytorch
для токенизации описаний, а также построения и обучения модели.
Во-первых, нам нужно преобразовать тексты в числа:
Словарный запас, который мы получаем в результате простой токенизации набора данных о поездах, огромен — почти 90 тысяч слов. Чем больше слов у нас есть, тем больше места для встраивания должна изучить модель. Чтобы упростить обучение, удалим из него самые редкие слова и оставим только те, которые встречаются не менее чем в 3% описаний. Это сократит словарный запас до 340 слов.
(полную реализацию CorpusDictionary
можно найти здесь )
corpus_dict = util.CorpusDictionary(data_train["description"]) corpus_dict.truncate_dictionary(min_frequency=0.03) data_train["vector"] = corpus_dict.transform(data_train["description"]) data_test["vector"] = corpus_dict.transform(data_test["description"]) print(data_train["vector"].head()) # 28453 [1, 1, 1, 1, 12, 1, 2, 1, 6, 1, 1, 1, 1, 1, 6,... # 48884 [1, 1, 13, 34, 3, 1, 1, 38, 12, 21, 2, 1, 37, ... # 36550 [1, 60, 61, 1, 62, 60, 61, 1, 1, 1, 1, 10, 1, ... # 34999 [1, 34, 1, 1, 75, 60, 61, 1, 1, 72, 1, 1, 67, ... # 19183 [1, 83, 1, 1, 87, 1, 1, 1, 12, 21, 42, 1, 2, 1... # Name: vector, dtype: object
Следующее, что нам нужно решить, — это общая длина векторов, которые мы собираемся подавать в качестве входных данных в RNN. Мы не хотим использовать полные векторы, поскольку самое длинное описание содержит 9,4 тыс. токенов.
Однако 95% описаний в наборе данных поезда не длиннее 352 токенов — это хорошая длина для обрезки. Что произойдет с более короткими описаниями?
Они будут дополнены индексом заполнения до общей длины.
print(max(data_train["vector"].apply(len))) # >>> 9388 print(int(np.quantile(data_train["vector"].apply(len), q=0.95))) # >>> 352
Далее нам нужно преобразовать целевые категории в векторы 0-1, чтобы вычислить потери и выполнить обратное распространение ошибки на каждом этапе обучения.
def get_target(label, total_labels=4): target = [0] * total_labels target[label_2_idx.get(label)] = 1 return target data_train["target"] = data_train["category"].apply(get_target) data_test["target"] = data_test["category"].apply(get_target)
Теперь мы готовы создать собственный набор данных и загрузчик данных pytorch
для использования в модели. Полную реализацию PaddedTextVectorDataset
можно найти здесь .
ds_train = util.PaddedTextVectorDataset( data_train["description"], data_train["target"], corpus_dict, max_vector_len=352, ) ds_test = util.PaddedTextVectorDataset( data_test["description"], data_test["target"], corpus_dict, max_vector_len=352, ) train_dl = DataLoader(ds_train, batch_size=512, shuffle=True) test_dl = DataLoader(ds_test, batch_size=512, shuffle=False)
Наконец, давайте построим модель.
Минимальная архитектура:
Начиная с небольших значений параметров (размер вектора внедрения, размер скрытого слоя в RNN, количество слоев RNN) и отсутствия регуляризации, мы можем постепенно усложнять модель, пока она не покажет явные признаки переобучения, а затем сбалансировать ее. регуляризация (выпадения в слое RNN и предпоследнем линейном слое).
class GRU(nn.Module): def __init__(self, vocab_size, embedding_dim, n_hidden, n_out): super().__init__() self.vocab_size = vocab_size self.embedding_dim = embedding_dim self.n_hidden = n_hidden self.n_out = n_out self.emb = nn.Embedding(self.vocab_size, self.embedding_dim) self.gru = nn.GRU(self.embedding_dim, self.n_hidden) self.dropout = nn.Dropout(0.3) self.out = nn.Linear(self.n_hidden, self.n_out) def forward(self, sequence, lengths): batch_size = sequence.size(1) self.hidden = self._init_hidden(batch_size) embs = self.emb(sequence) embs = pack_padded_sequence(embs, lengths, enforce_sorted=True) gru_out, self.hidden = self.gru(embs, self.hidden) gru_out, lengths = pad_packed_sequence(gru_out) dropout = self.dropout(self.hidden[-1]) output = self.out(dropout) return F.log_softmax(output, dim=-1) def _init_hidden(self, batch_size): return Variable(torch.zeros((1, batch_size, self.n_hidden)))
Мы будем использовать оптимизатор Adam
и cross_entropy
в качестве функции потерь.
vocab_size = len(corpus_dict.word_to_idx) emb_dim = 4 n_hidden = 15 n_out = len(label_2_idx) model = GRU(vocab_size, emb_dim, n_hidden, n_out) opt = optim.Adam(model.parameters(), 1e-2) util.fit( model=model, train_dl=train_dl, test_dl=test_dl, loss_fn=F.cross_entropy, opt=opt, epochs=35 ) # >>> Train loss: 0.3783 # >>> Val loss: 0.4730
Эта модель показала сбалансированную точность 84,3% в наборе оценочных данных. Ух ты, какой прогресс!
Основным недостатком обучения модели RNN с нуля является то, что ей приходится самой изучать значение слов — это задача слоя внедрения. Предварительно обученные модели word2vec
доступны для использования в качестве готового слоя внедрения, что уменьшает количество параметров и придает токенам гораздо больше смысла. Давайте воспользуемся одной из моделей word2vec
, доступных в pytorch
— glove, dim=300
.
Нам нужно внести лишь незначительные изменения в создание набора данных — теперь мы хотим создать вектор предварительно определенных индексов glove
для каждого описания и архитектуры модели.
ds_emb_train = util.PaddedTextVectorDataset( data_train["description"], data_train["target"], emb=glove, max_vector_len=max_len, ) ds_emb_test = util.PaddedTextVectorDataset( data_test["description"], data_test["target"], emb=glove, max_vector_len=max_len, ) dl_emb_train = DataLoader(ds_emb_train, batch_size=512, shuffle=True) dl_emb_test = DataLoader(ds_emb_test, batch_size=512, shuffle=False)
import torchtext.vocab as vocab glove = vocab.GloVe(name='6B', dim=300) class LSTMPretrained(nn.Module): def __init__(self, n_hidden, n_out): super().__init__() self.emb = nn.Embedding.from_pretrained(glove.vectors) self.emb.requires_grad_ = False self.embedding_dim = 300 self.n_hidden = n_hidden self.n_out = n_out self.lstm = nn.LSTM(self.embedding_dim, self.n_hidden, num_layers=1) self.dropout = nn.Dropout(0.5) self.out = nn.Linear(self.n_hidden, self.n_out) def forward(self, sequence, lengths): batch_size = sequence.size(1) self.hidden = self.init_hidden(batch_size) embs = self.emb(sequence) embs = pack_padded_sequence(embs, lengths, enforce_sorted=True) lstm_out, (self.hidden, _) = self.lstm(embs) lstm_out, lengths = pad_packed_sequence(lstm_out) dropout = self.dropout(self.hidden[-1]) output = self.out(dropout) return F.log_softmax(output, dim=-1) def init_hidden(self, batch_size): return Variable(torch.zeros((1, batch_size, self.n_hidden)))
И мы готовы тренироваться!
n_hidden = 50 n_out = len(label_2_idx) emb_model = LSTMPretrained(n_hidden, n_out) opt = optim.Adam(emb_model.parameters(), 1e-2) util.fit(model=emb_model, train_dl=dl_emb_train, test_dl=dl_emb_test, loss_fn=F.cross_entropy, opt=opt, epochs=11)
Теперь мы получаем сбалансированную точность 93,7% в наборе данных eval. Ву!
Современные современные модели работы с последовательностями — это трансформеры. Однако для обучения трансформатора с нуля нам потребуются огромные объемы данных и вычислительных ресурсов. Здесь мы можем попробовать настроить одну из предварительно обученных моделей для достижения нашей цели. Для этого нам нужно загрузить предварительно обученную модель BERT и добавить отсев и линейный слой, чтобы получить окончательный прогноз. Рекомендуется обучать настроенную модель в течение 4 эпох. Для экономии времени я тренировал всего 2 дополнительные эпохи — на это у меня ушло 40 минут.
from transformers import BertModel class BERTModel(nn.Module): def __init__(self, n_out=12): super(BERTModel, self).__init__() self.l1 = BertModel.from_pretrained('bert-base-uncased') self.l2 = nn.Dropout(0.3) self.l3 = nn.Linear(768, n_out) def forward(self, ids, mask, token_type_ids): output_1 = self.l1(ids, attention_mask = mask, token_type_ids = token_type_ids) output_2 = self.l2(output_1.pooler_output) output = self.l3(output_2) return output
ds_train_bert = bert.get_dataset( list(data_train["description"]), list(data_train["target"]), max_vector_len=64 ) ds_test_bert = bert.get_dataset( list(data_test["description"]), list(data_test["target"]), max_vector_len=64 ) dl_train_bert = DataLoader(ds_train_bert, sampler=RandomSampler(ds_train_bert), batch_size=batch_size) dl_test_bert = DataLoader(ds_test_bert, sampler=SequentialSampler(ds_test_bert), batch_size=batch_size)
b_model = bert.BERTModel(n_out=4) b_model.to(torch.device("cpu")) def loss_fn(outputs, targets): return torch.nn.BCEWithLogitsLoss()(outputs, targets) optimizer = optim.AdamW(b_model.parameters(), lr=2e-5, eps=1e-8) epochs = 2 scheduler = get_linear_schedule_with_warmup( optimizer, num_warmup_steps=0, num_training_steps=total_steps ) bert.fit(b_model, dl_train_bert, dl_test_bert, optimizer, scheduler, loss_fn, device, epochs=epochs) torch.save(b_model, "models/bert_fine_tuned")
Журнал тренировок:
2024-02-29 19:38:13.383953 Epoch 1 / 2 Training... 2024-02-29 19:40:39.303002 step 40 / 305 done 2024-02-29 19:43:04.482043 step 80 / 305 done 2024-02-29 19:45:27.767488 step 120 / 305 done 2024-02-29 19:47:53.156420 step 160 / 305 done 2024-02-29 19:50:20.117272 step 200 / 305 done 2024-02-29 19:52:47.988203 step 240 / 305 done 2024-02-29 19:55:16.812437 step 280 / 305 done 2024-02-29 19:56:46.990367 Average training loss: 0.18 2024-02-29 19:56:46.990932 Validating... 2024-02-29 19:57:51.182859 Average validation loss: 0.10 2024-02-29 19:57:51.182948 Epoch 2 / 2 Training... 2024-02-29 20:00:25.110818 step 40 / 305 done 2024-02-29 20:02:56.240693 step 80 / 305 done 2024-02-29 20:05:25.647311 step 120 / 305 done 2024-02-29 20:07:53.668489 step 160 / 305 done 2024-02-29 20:10:33.936778 step 200 / 305 done 2024-02-29 20:13:03.217450 step 240 / 305 done 2024-02-29 20:15:28.384958 step 280 / 305 done 2024-02-29 20:16:57.004078 Average training loss: 0.08 2024-02-29 20:16:57.004657 Validating... 2024-02-29 20:18:01.546235 Average validation loss: 0.09
Наконец, точно настроенная модель BERT показывает колоссальную сбалансированную точность 95,1% в наборе оценочных данных.
Мы уже составили список факторов, на которые следует обратить внимание, чтобы сделать окончательный осознанный выбор.
Вот диаграммы, показывающие измеряемые параметры:
Хотя точно настроенный BERT лидирует по качеству, RNN с предварительно обученным слоем внедрения LSTM+EMB
находится на втором месте, отставая всего на 3% от автоматического назначения категорий.
С другой стороны, время вывода точно настроенного BERT в 14 раз больше, чем LSTM+EMB
. Это приведет к дополнительным затратам на обслуживание серверной части, которые, вероятно, перевесят преимущества, которые дает точно настроенный BERT
по сравнению с LSTM+EMB
.
Что касается совместимости, наша базовая модель логистической регрессии на сегодняшний день является наиболее интерпретируемой, и любая нейронная сеть проигрывает ей в этом отношении. В то же время базовый уровень, вероятно, наименее масштабируем — добавление категорий снизит и без того низкое качество базового уровня.
Несмотря на то, что BERT кажется суперзвездой благодаря своей высокой точности, в конечном итоге мы выбираем RNN с предварительно обученным слоем внедрения. Почему? Он довольно точен, не слишком медленный и не слишком усложняется, когда дело становится большим.
Надеюсь, вам понравился этот практический пример. Какую модель вы бы выбрали и почему?