Каждый день я открываю свой почтовый ящик и вижу десятки непрочитанных писем от незнакомых мне людей, которые просят всего 15 минут, чтобы они помогли мне решить ту или иную проблему.

Популяризованная в 2011 году книгой Аарона Росса Predictable Revenue, концепция холодного электронного письма потенциальным клиентам B2B с просьбой о встрече стала основной тактикой роста компаний B2B SaaS по всему миру. В самом деле, секрет раскрыт, и все - и, очевидно, их матери - рассылают электронные письма с поисковыми предложениями, чтобы стимулировать бизнес. (См., Например, 30 шаблонов электронных писем для потенциальных клиентов, гарантирующих начало отношений.) И не зря - это работает!

Так что же происходит, когда каждая компания B2B отправляет электронные письма всем потенциальным клиентам по всему миру? Наши почтовые ящики становятся более шумными, что затрудняет сосредоточение внимания на важных электронных письмах.

Что принес прогнозируемый доход?

Даже надежные почтовые сервисы, такие как Gmail и Outlook, не классифицируют эти электронные письма SDR как спам. Это понятно, учитывая, что SDR не продают виагру, не запрашивают денежные переводы и тому подобное. А как насчет фильтра Gmail "Промоакции"? Никакой помощи. Торговые представители часто отправляют электронные письма с использованием электронной подписи Gmail или Outlook своей компании с помощью технологии поддержки продаж, такой как Outreach, которые, по-видимому, обходят обычные фильтры информационных бюллетеней.

Если этот флажок не установлен, электронные письма SDR будут продолжать вторгаться в почтовые ящики и уменьшать внимание к релевантному контенту.

Восстановление папки "Входящие"

В ответ на эту дилемму - но на самом деле как предлог для развития моих навыков работы с NLP, Docker и микросервисами - я спроектировал, закодировал и развернул детектор электронной почты SDR, доступный через конечную точку API.

Система подключается к Gmail пользователя через OAuth, определяет, является ли входящая электронная почта from_SDR, и перемещает такие электронные письма в специальную папку «Прочитать позже». Если электронное письмо не from_SDR, то секретное письмо остается в основном почтовом ящике.

Создание детектора электронной почты SDR

Детектирование почтового спама было ближайшим аналогом моему проекту. Здесь есть много подходов, начиная от простого наивного байеса с использованием векторизации подсчета мешка слов до сложных моделей глубокого обучения с использованием двунаправленных рекуррентных нейронных сетей для встраивания слов. Хотя я пробовал все вышеперечисленное, в этой статье я сосредоточусь на реализации Linear SVM с использованием векторизации TF-IDF.

Получение данных обучения

Каким бы ни был подход, наиболее важной частью прикладного машинного обучения является (1) получение хороших данных и (2) подготовка этих данных для обучения и тестирования.

Получить данные для обучения было относительно просто. Я отфильтровал свой почтовый ящик по ключевым словам, связанным с продажами, и просмотрел тысячи писем. Если письмо было от представителя отдела продаж (SDR), я добавил к нему метку SDR. Примерно через восемь часов я пометил 1000 писем SDR в своем почтовом ящике:

Затем я получил 4000 писем без SDR из своего почтового ящика. Я намеренно увеличил выборку электронных писем без SDR, чтобы воспроизвести дисбаланс классов, который возникает в реальном мире, при этом помня о необходимости иметь достаточно SDR сигнала (например, 20% моих данных), чтобы мой классификатор узнал что-то значимое.

Затем я запустил записную книжку Google Colab, чтобы загрузить электронные письма в Pandas Dataframe.

Объект service создает соединение с Gmail API и целевой почтовый ящик. (Обратите внимание, что вам нужно будет настроить учетную запись Google Cloud Platform, чтобы зарегистрировать приложение и получить client_secret.json.)

Затем я написал функцию для получения ряда сообщений, соответствующих запросу:

Вышеупомянутая функция вернула массив идентификаторов сообщений, которые нужно было итеративно получать из Gmail API. Для этого я написал следующую функцию, которая вызывает каждое сообщение и анализирует тело, от, тему, дату / время и другие интересные поля. Учитывая возможность запрашивать метки (например, label:SDR для получения всех электронных писем SDR в моем почтовом ящике), я заранее знал, был ли массив электронной почты y_true=1 или y_true=0, что является параметром, который моя функция использует для маркировки каждой строки в фрейме данных:

Но есть еще одна функция, которую необходимо определить, и она была, безусловно, самой сложной: get_email_data(message_id). Эта функция рабочей лошадки преобразует закодированные ответы Gmail в текст, который можно проанализировать и сохранить в Dataframe. Это ни в коем случае не идеально, поскольку я пришел, чтобы узнать, насколько сложными и разнообразными могут быть электронные письма. Вы заметите вложенную логику и многоэтапный обход, необходимые для правильного анализа электронных писем, учитывая, что электронные письма кодируются по-разному, если они содержат вложения, HTML, открытый текст и другие крайние случаи.

Наконец, я импортировал pickle, чтобы сохранить свои фреймы данных, что помогло мне сэкономить вызовы API и потратить время на повторную выборку тех же данных:

После того, как основы были завершены, я решил немного поработать над функциями, добавив флаг cold_contact, равный 1, если пользователь почтового ящика никогда не отправлял электронное письмо отправителю:

Я также экспериментировал с другими функциями (такими как несовпадения отправителя / return_path и подсчет количества писем, отправленных за предыдущие 90, 60, 30 дней и т. Д.), Но я избавлю вас от необходимости просматривать этот код, поскольку они не значительно улучшить характеристики моей модели.

Вот несколько примеров строк и столбцов из моего фрейма данных:

Обучение детектора электронной почты SDR с использованием обработки естественного языка

Вооружившись данными электронной почты, мне пришлось преобразовать электронные письма в формат, готовый для классификации. Как отмечалось выше, есть много способов сделать это, и я решил представлять тело каждого электронного письма, используя термин "векторизация документа с обратной частотой".

Процитируем Одностраничное руководство:

Обычно вес tf-idf состоит из двух членов: первый вычисляет нормализованную частоту термина (TF), иначе. количество раз, когда слово встречается в документе, деленное на общее количество слов в этом документе; второй член - это обратная частота документов (IDF), вычисляемая как логарифм количества документов в корпусе, деленный на количество документов, в которых встречается конкретный термин.
. . .
Рассмотрим документ, содержащий 100 слов, в котором слово «кошка» встречается 3 раза. Тогда термин частота (т.е. tf) для кота (3/100) = 0,03. Теперь предположим, что у нас есть 10 миллионов документов, и слово кошка встречается в одной тысяче из них. Затем обратная частота документа (т.е. idf) вычисляется как log (10,000,000 / 1,000) = 4. Таким образом, вес tf-idf является произведением этих величин: 0,03 * 4 = 0,12.

Представьте себе каждое электронное письмо как строку в матрице, в которой каждое слово электронного письма представляет собой функцию (например, столбец), значение которой задается формулой выше. "Например":

Вот проблема: хочу ли я хранить (то есть проблемы с памятью) и анализировать (то есть время процессора) миллионы слов / столбцов данных? Нет. Хитрость заключается в том, чтобы предварительно обработать электронные письма, чтобы я получал меньше сигнальных слов с более высоким содержанием.

Предварительная обработка текста электронной почты

Джей Патель предлагает подробное описание и примеры преобразования необработанного текста в форму, которая может быть векторизована и использована алгоритмами машинного обучения. Я последовал его примеру и закодировал эти функции для удаления HTML, знаков препинания и слабых сигнальных слов:

Обратите внимание на lemmatizer в приведенном выше коде. Эта концепция выходит за рамки этой публикации, но стоит упомянуть, что лемматизация - это еще один способ уменьшить размерность написанного текста так, чтобы запускать, запускать, run, running, runner и т. д. просто представлены одним словом: run.

Чтобы применить вышеперечисленные функции на практике, я использовал ColumnTransformers, как описано в этом сообщении Сэма Т.. Здесь я определил преобразователь, который принимает фрейм данных со столбцом body и обрабатывает каждую строку с помощью функций очистки, указанных выше:

Важно отметить, что этот преобразователь использовал remainder='passthrough' для сохранения и распространения других столбцов фрейма данных по конвейеру. Очистить тело писем стало проще простого. Мне просто нужно было запустить функцию clean_text_CT_passthrough.transform(some_dataframe).

Но что еще лучше, я мог связать несколько шагов предварительной обработки с помощью SKLearn Pipelines и ColumnTransformers. Например, в этом фрагменте я изменил масштаб одного столбца моего фрейма данных, чтобы он был пропорционален диапазону значений, вычисленному с помощью векторизации TF-IDF:

Например, в одной из версий я попытался добавить функцию, позволяющую подсчитывать количество слов в каждом письме. В то время как векторизация текста имела порядок 0 → 1, количество слов варьировалось от 0 до 1500, и без масштабирования модель провалилась из-за чрезмерного акцента на функции подсчета слов. Вышеупомянутая функция изменяет масштаб количества слов до диапазона от 0 до 1, как и в других столбцах.

Наконец, я выполнил векторизацию слов и объединил столбцы в единый вектор, готовый для классификации.

Получившийся объект classification_pipe имел .fit, .predict и другие функции, которые вы ожидаете от классификаторов SKLearn. Они были доступны, потому что я определил классификатор в строках 8–9 и добавил его в конвейер в строке 24.

Собираем все вместе

Я загрузил данные электронной почты из сохраненного рассола и преобразовал тело в string. Затем я создал разделение данных на поезд / тест, передав stratify=y_df.y_true функции train_test_split, чтобы мои разбиения были выбраны с использованием такого же дисбаланса классов, что и моя целевая переменная. Затем я вызвал функцию classification_pipe.fit, используя данные обучения. Наконец, я оценил производительность моей модели, используя баллы F1 среди других показателей:

Вот результаты моей модели после оптимизации предварительной обработки текста, векторизатора TF-IDF и параметров SVM:

Оценка F1: 98,34%
AP: 95,06%
Точность: 97,09%
Отзыв: 97,40%

Хотя всегда есть место для дополнительной оптимизации, я чувствовал, что это хорошая точка остановки, учитывая высокий балл F1 (98,34) и значительное улучшение по сравнению с тем, с чего я начал (83,44).

Следующие шаги: развертывание модели

Чтобы построить систему, описанную в начале этого поста, мой апплет Gmail должен вызывать эту модель через API. Об этом и говорится в следующем посте из этой серии. Там я помещаю свою модель в контейнер, развертываю ее в AWS ECR и использую Chalice для создания конечной точки API, доступной через Интернет.

Чтобы избежать длинных фрагментов кода, приведенных выше, я поместил большинство операторов import в this Gist. Обязательно просмотрите и включите их, если вы запустите код, описанный здесь.

Слишком много людей, которых нужно благодарить за то, что этот пост стал возможен. Вот лишь некоторые из них, которые существенно повлияли на мое приключение: Си Хуай ГанНиавес Байес и классификаторы спама с глубоким обучением), Пол Бланкли (о показателях оценки), Микко Охтамааразборе писем), Ален Спине (объяснение частей письма), Зак Стюарт (конвейеры), Оливье Гризель , Питер Преттенхофер и Матье Блондель (о использовании конвейеров, преобразователей и GridSearchCV), Сэм Т.конвейерах с настраиваемыми преобразователями), Кен Syme (о сочетании текстовых и табличных данных в ColumnTransformer), Джей М. Патель (о предварительной обработке текста для НЛП), Райан Крэнфилл добавлении пользовательских функций для предварительной обработки текст в конвейере ), Марцин Заблоцкий (об использовании фреймов данных в конвейерах SKLearn ), Мишель Фуллвуд конвейерах и объединениях функций ) и Бенджамин Бенгфорт, Ребекка Билбро, Тони Охедавекторизации текста с помощью конвейеров). Спасибо всем, что нашли время поделиться своими знаниями!