Спам становится огромной проблемой. В прошлом году 53,5% всего почтового трафика во всем мире приходилось на спам-сообщения, причем наиболее распространенными темами были здравоохранение и свидания.

Представьте, сколько здесь тратится впустую пропускная способность и электричество!

Итак, давайте посмотрим, сможем ли мы решить эту проблему. В этой статье я собираюсь создать приложение C # с CNTK и NET Core, которое может предсказать, является ли какое-либо сообщение спамом.

CNTK - это набор инструментов Microsoft Cognitive Toolkit, тензорная библиотека, аналогичная TensorFlow. Он может создавать, обучать и запускать глубокие нейронные сети для регрессии, классификации и многих других задач машинного обучения.

А NET Core - это многоплатформенная платформа Microsoft NET Framework, работающая в Windows, OS / X и Linux. Это будущее кроссплатформенной разработки NET.

Первое, что мне нужно, это файл данных с большим количеством сообщений, правильно помеченных как спам или не спам. Я буду использовать коллекцию SMS-сообщений, собранную Кэролайн Тэгг в ее кандидатской диссертации 2009 года. В этом наборе данных 5574 сообщения.

Вы можете скачать набор данных здесь. Для загрузки вам понадобится учетная запись Kaggle. Сохраните файл как spam.csv.

Файл выглядит так:

Это файл TSV, содержащий всего 2 столбца информации:

  • Ярлык: «спам» для спам-сообщения и «ветчина» для обычного сообщения.
  • Сообщение: полный текст SMS-сообщения.

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

Давайте начнем. Вот как настроить новый консольный проект в NET Core:

$ dotnet new console -o SpamDetection
$ cd SpamDetection

Далее мне нужно установить необходимые пакеты:

$ dotnet add package Microsoft.ML
$ dotnet add package CNTK.GPU
$ dotnet add package XPlot.Plotly
$ dotnet add package Fsharp.Core

Microsoft.ML - это пакет машинного обучения Microsoft. Мы будем использовать для загрузки и обработки данных из набора данных.

Библиотека CNTK.GPU - это Cognitive Toolkit от Microsoft, который может обучать и запускать глубокие нейронные сети.

А Xplot.Plotly - отличная библиотека для построения графиков, основанная на Plotly. Библиотека разработана для F #, поэтому нам также необходимо использовать библиотеку Fsharp.Core.

Пакет CNTK.GPU будет обучать и запускать глубокие нейронные сети с помощью вашего графического процессора. Для этого вам понадобится графический процессор NVidia и графические драйверы Cuda.

Если у вас нет графического процессора NVidia или подходящих драйверов, библиотека откатится и вместо этого будет использовать процессор. Это сработает, но обучение нейронных сетей займет значительно больше времени.

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

Вы можете загрузить файлы CNTKUtil и сохранить их в новой папке CNTKUtil на том же уровне, что и папка вашего проекта.

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

$ dotnet add reference ..\CNTKUtil\CNTKUtil.csproj

Теперь я готов начать писать код. Я отредактирую файл Program.cs с помощью кода Visual Studio и добавлю следующий код:

Класс SpamData содержит все данные для одного спам-сообщения. Обратите внимание, как каждое поле помечено атрибутом LoadColumn, который сообщает коду загрузки данных TSV, из какого столбца импортировать данные.

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

Я вернусь к этому преобразованию позже. А пока я добавлю сюда класс, который будет содержать преобразованный текст:

Снова появляется Ярлык, но обратите внимание, что сообщение теперь преобразовано в VBuffer и сохранено в поле Функции.

Тип VBuffer - это разреженный вектор. Он будет хранить очень большой вектор, в основном с нулями и лишь с несколькими ненулевыми значениями. Хорошая особенность типа VBuffer заключается в том, что он хранит только ненулевые значения. Нули не сохраняются и не занимают места в памяти.

Метод GetFeatures вызывает DenseValues ​​ для возврата полного вектора и возвращает его как float [], которое распознает наша нейронная сеть.

И есть метод GetLabel, который возвращает 1, если сообщение является спамом (обозначено полем Label, содержащим слово «спам»), и 0, если сообщение не является спамом.

Функции представляют текст, преобразованный в разреженный вектор, который мы будем использовать для обучения нейронной сети, а метка - это выходная переменная, которую мы пытаемся предсказать. Итак, здесь мы обучаем закодированному тексту, чтобы предсказать, является ли этот текст спамом или нет.

Пришло время написать основной метод программы:

При работе с библиотекой ML.NET нам всегда необходимо настроить контекст машинного обучения, представленный классом MLContext.

Код вызывает метод LoadFromTextFile для загрузки данных CSV в память. Обратите внимание на аргумент типа SpamData, который сообщает методу, какой класс использовать для загрузки данных.

Затем я использую TrainTestSplit для разделения данных на обучающий раздел, содержащий 70% данных, и тестовый раздел, содержащий 30% данных.

Обратите внимание, что здесь я отклоняюсь от обычного деления 80–20. Это связано с тем, что файл данных довольно мал, и поэтому 20% данных просто недостаточно для тестирования нейронной сети.

Пришло время создать конвейер для преобразования текста в разреженные данные с векторной кодировкой. Я буду использовать компонент FeaturizeText в библиотеке машинного обучения ML.NET:

Конвейеры машинного обучения в ML.NET создаются путем наложения компонентов преобразования. Здесь я использую единственный компонент, FeaturizeText, который преобразует текстовые сообщения в SpamData.Message в разреженные данные с векторной кодировкой в ​​новом столбце под названием «Features».

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

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

Я вызываю метод Fit для инициализации конвейера, а затем дважды вызываю Transform, чтобы преобразовать текст в разделах обучения и тестирования.

Наконец, я вызываю CreateEnumerable, чтобы преобразовать данные обучения и тестирования в перечисление экземпляров ProcessedData. Итак, теперь у меня есть данные для обучения в обучении и данные для тестирования в тестировании. Оба являются перечислением экземпляров ProcessedData.

Но CNTK не может обучаться перечислению экземпляров классов. Требуется float [] [] для функций и float [] для меток.

Итак, мне нужно настроить четыре массива с плавающей запятой:

Эти выражения LINQ создают четыре массива, содержащих данные функций и меток для разделов обучения и тестирования.

Теперь мне нужно сообщить CNTK, какую форму имеют входные данные, на которых я буду тренировать нейронную сеть, и какую форму будут иметь выходные данные нейронной сети:

Я не знаю заранее, сколько измерений создаст компонент FeaturizeText, поэтому просто проверяю ширину массива training_data.

Первый метод Var сообщает CNTK, что моя нейронная сеть будет использовать в качестве входных данных одномерный тензор значений с плавающей запятой nodeCount. Эта форма соответствует массиву, возвращаемому методом ProcessedData.GetFeatures.

А второй метод Var сообщает CNTK, что я хочу, чтобы моя нейронная сеть выводила одно значение с плавающей запятой. Эта фигура соответствует единственному значению, возвращаемому методом ProcessedData.GetLabel.

Мой следующий шаг - спроектировать нейронную сеть.

Я буду использовать глубокую нейронную сеть с входным слоем из 16 узлов, скрытым слоем из 16 узлов и выходным слоем с одним узлом. Я буду использовать функцию активации ReLU для входного и скрытого слоев и активацию Sigmoid для выходного слоя.

Сигмоидальная функция принудительно устанавливает выходной сигнал сети регрессии в диапазон 0..1, что означает, что я могу рассматривать число как вероятность двоичной классификации. Таким образом, мы можем превратить любую сеть регрессии в сеть двоичной классификации, просто добавив сигмовидную функцию активации к выходному слою.

Вот как построить эту нейронную сеть:

Каждый вызов Dense добавляет к сети новый плотный слой прямой связи. Я складываю два слоя, используя активацию ReLU, а затем добавляю последний слой с одним узлом, используя активацию сигмоида.

Затем я использую метод ToSummary для вывода описания архитектуры нейронной сети на консоль.

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

Я буду использовать BinaryCrossEntropy в качестве функции потерь, потому что это стандартный показатель для измерения потерь при двоичной классификации.

А я буду отслеживать ошибку с помощью метрики BinaryClassificationError. Это количество раз (выраженное в процентах), когда прогнозы модели оказываются неверными. Ошибка 0 означает, что прогнозы все время верны, а ошибка 1 означает, что прогнозы все время неверны.

Затем мне нужно решить, какой алгоритм использовать для обучения нейронной сети. Есть много возможных алгоритмов, полученных из Gradient Descent, которые мы можем использовать здесь.

Я собираюсь использовать AdamLearner. Вы можете узнать больше об алгоритме Адама здесь: https: //machinelearningmastery.com/adam ...

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

Мы почти готовы к тренировке. Мой последний шаг - настроить трейнер и оценщик для расчета потерь и ошибок в каждую тренировочную эпоху:

Метод GetTrainer настраивает трейнер, который будет отслеживать потери и ошибки для обучающего раздела. А GetEvaluator настроит оценщик, отслеживающий ошибку в тестовом разделе.

Теперь я наконец готов приступить к обучению нейронной сети!

Мне нужно добавить следующий код:

Я обучаю сеть для 10 эпох, используя размер пакета 64. Во время обучения я буду отслеживать потери и ошибки в loss, trainingError и testingError массивы.

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

Обратите внимание, что ошибка и точность связаны: точность = 1 - ошибка. Так что я также сообщаю окончательную точность нейронной сети.

Вот код для обучения нейронной сети. Это должно быть внутри цикла for:

Последовательность Index (). Shuffle (). Batch () рандомизирует данные и разбивает их на набор пакетов по 64 записи. Второй аргумент Batch () - это функция, которая будет вызываться для каждой партии.

Внутри пакетной функции я дважды вызываю GetBatch, чтобы получить пакет функций и соответствующий пакет меток. Затем я вызываю TrainBatch, чтобы обучить нейронную сеть на этих двух пакетах обучающих данных.

Метод TrainBatch возвращает потерю и ошибку, но только для обучения на пакете из 64 записей. Поэтому я просто складываю все эти значения и делю их на количество пакетов в наборе данных. Это дает мне среднюю потерю и ошибку для прогнозов в обучающем разделе за текущую эпоху, и я сообщаю об этом на консоль.

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

Этот код находится внутри цикла эпох и сразу под обучающим кодом:

Мне не нужно перемешивать данные для тестирования, поэтому теперь я могу напрямую вызывать Batch. Я снова вызываю GetBatch, чтобы получить пакеты функций и меток, но обратите внимание, что теперь я предоставляю массивы testing_data и testing_labels.

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

Это дает мне среднюю ошибку прогнозов нейронной сети на тестовом разделе для этой эпохи.

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

Этот код создает график с двумя графиками разброса. Первый отображает значения trainingError, а второй - значения testingError.

Наконец, я использую File.WriteAllText, чтобы записать график на диск в виде файла HTML.

Теперь я готов создать и запустить приложение!

Сначала мне нужно собрать библиотеку CNTKUtil, выполнив эту команду в папке CNTKUtil:

$ dotnet build -o bin/Debug/netcoreapp3.0 -p:Platform=x64

Это создаст проект CNKTUtil. Обратите внимание, как я указываю платформу x64, потому что для библиотеки CNTK требуется 64-разрядная сборка.

Теперь мне нужно запустить эту команду в папке SpamDetection:

$ dotnet build -o bin/Debug/netcoreapp3.0 -p:Platform=x64

Это создаст приложение. Обратите внимание, как я снова указываю платформу x64.

Теперь я могу запустить приложение:

$ dotnet run

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

Вот нейронная сеть, обучаемая на моем ноутбуке:

И вот результаты:

Итоговая ошибка классификации составляет 0 при обучении и 0,010 при тестировании. Это соответствует конечной точности тестирования 0,99. Это означает, что нейронная сеть делает 99 правильных прогнозов на каждые 100 сообщений.

Это кажутся потрясающими результатами, заметили, как кривые обучения и тестирования начинают расходиться в эпоху 2? Ошибка обучения продолжает стремиться к нулю, а ошибка тестирования - 0,01. Это классический перебор.

Переоснащение означает, что сообщения слишком сложны для обработки моделью. Этот режим недостаточно сложен, чтобы фиксировать сложность шаблонов в данных.

И этого следовало ожидать. Обработка английского текста - серьезная проблема и область активных исследований даже сегодня. Простая нейронная сеть с 32 узлами не сможет генерировать точные прогнозы спама.

Что, если мы увеличим сложность нейронной сети? Давайте удвоим количество узлов во входном и скрытом слоях:

Теперь нейронная сеть имеет 766 881 настраиваемый параметр для обучения в каждую эпоху. Это огромная сеть, но каковы будут результаты?

Что ж, зацените:

Ничего не изменилось. Я все еще получаю ошибку обучения, равную нулю, и ошибку тестирования, равную 0,01. И кривые все еще расходятся, теперь в эпоху 1.

Давайте сделаем все возможное и увеличим количество узлов до 512:

Теперь нейронная сеть имеет поразительные 12515841 обучаемый параметр.

И вот результаты:

Опять без изменений. Что тут происходит?

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

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

Например, во фрагменте текста «не очень хорошо» значение «хорошо» инвертируется наличием «не очень». Если я просто проверю наличие слова «хорошо», я получаю совершенно неверное представление о значении предложения.

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

Так что ты думаешь?

Готовы ли вы начать писать приложения для машинного обучения C # с помощью CNTK?

Эта статья основана на домашнем задании из моего курса машинного обучения: Глубокое обучение с C # и C NTK .