Привет! Сегодня я расскажу вам больше о том, что я сделал для создания спам-фильтра с использованием Наивного Байеса для обнаружения спам-данных из этого набора данных на kaggle с помощью машинного обучения UCI, а также с использованием TextAnalysis.jl на Юлии. .

Я начал с просмотра документов TextAnalysis.jl, чтобы лучше понять, как именно работает Классификатор NaiveBayes.

using TextAnalysis: NaiveBayesClassifier, fit!, predict
m = NaiveBayesClassifier([:legal, :financial])
fit!(m, "this is financial doc", :financial)
fit!(m, "this is legal doc", :legal)
predict(m, "this should be predicted as a legal document")

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

В данном случае это были :legal и :financial. Я также узнал, что мы обучаем модель, сопоставляя соответствующие данные с функцией fit!, где она принимает аргументы самой модели, строку данных, которые мы пытаемся обучить, и класс, к которому принадлежат данные. Данные здесь представляют собой строку, например “this is financial doc”, а класс, к которому она принадлежит, в данном случае :financial.

Наконец, я узнал, что функция predict позволяет нам вводить строку данных и использует алгоритм NaiveBayesClassifier для предсказания того, к какому классу принадлежит строка, на основе строк данных, обученных до использования функции fit!. Функция predict принимает аргументы самой модели, а также строку данных, которые мы пытаемся предсказать.

Мой первый подход к проблеме заключался в том, что я подумал, что было бы неплохо сначала импортировать все данные. Имея опыт работы с пакетами CSV.jl и DataFrames.jl, я знаком с импортом данных.

using CSV, DataFrames
spamdata = DataFrame(CSV.read("spam.csv"; allowmissing=:none))
---------------------------------------------------------------
julia> showall(spamdata)
5572×5 DataFrame
│ Row  │ v1     │
│      │ String │
├──────┼────────┤
│ 1    │ ham    │
│ 2    │ ham    │
│ 3    │ spam   │
│ 4    │ ham    │
│ 5    │ ham    │
│ 6    │ spam   │
│ 7    │ ham    │
│ 8    │ ham    │
│ 9    │ spam   │
│ 10   │ spam   │

На этом изображении ниже показана структура исходного файла .csv с данными.

CSV-файл имеет два столбца. v2 — это строка данных, которую мы хотим использовать для обучения, а v1 — это класс конкретной строки данных, соответствующей v2.

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

for row in eachrow(spamdata)
    if row.v1 == "ham"
        println("ham")
    elseif row.v1 == "spam"
        println("spam")
    end
end

Это был успех, и я закончил тем, что напечатал ветчину и спам!

ham
spam
ham
spam
ham
ham
spam
spam
ham
spam
⋮

Теперь, когда это не так, я могу определить свою модель с помощью функции NaiveBayesClassifier. Я хочу определить 2 класса, :ham и :spam.

using TextAnalysis: NaiveBayesClassifier, fit!, predict
m = NaiveBayesClassifier([:ham, :spam])

Далее я хочу начать обучение моей модели. Как видно из исходной структуры файла .csv, v2 — это строка, которую мы пытаемся обучить. Объединив цикл for с функцией fit!, я сделал следующее, чтобы попытаться обучить все имеющиеся у нас данные.

using CSV, DataFrames
using TextAnalysis: NaiveBayesClassifier, fit!, predict
spamdata = DataFrame(CSV.read("spam.csv"; allowmissing=:none))
global m = NaiveBayesClassifier([:ham, :spam])
for row in eachrow(spamdata)
    if row.v1 == "ham"
        fit!(m, row.v2, :ham)
    elseif row.v1 == "spam"
        fit!(m, row.v2, :spam)
    end
end

Но когда я запустил это, я получил следующую ошибку: LoadError: Base.InvalidCharError{Char}('\xe5\xa3')

Именно тогда я понял, что набор данных содержит недопустимые символы в некоторых строках в столбце v2. Чтобы устранить эту ошибку, нам нужно отфильтровать неподдерживаемые символы, используя следующую функцию: filter(isvalid, <string>)

for row in eachrow(spamdata)
    if row.v1 == "ham"
        fit!(m, filter(isvalid, row.v2), :ham)
    elseif row.v1 == "spam"
        fit!(m, filter(isvalid, row.v2), :spam)
    end
end

Когда я заменил строку row.v2 на filter(isvalid, row.v2) и снова запустил программу, ошибок не появилось. Поэтому модель успешно прошла обучение и пока все хорошо!

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

prediction1 = predict(m, "hello my name is kfung")
prediction2 = predict(m, "text 31845 to get a free phone")
println(prediction1)
println(prediction2)

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

Результаты, которые я получил, приведены ниже:

Dict(:spam => 0.013170434049325023, :ham => 0.986829565950675)
Dict(:spam => 0.9892304346396908, :ham => 0.010769565360309069)

Как мы можем сказать, предсказания были довольно точными, так как первое предсказание имело значение :ham близкое к 1, что означает, что это, скорее всего, нежелательное сообщение, а второе предсказание имело :spam value близкое к 1, что означает, что это, скорее всего, спам-сообщение. Именно то, что мы ожидали.

Но пользователи, которые не знакомы со словарями или синтаксисом Julia, могут не понимать, что означают приведенные выше словари. Я изменил код так, чтобы он проверял значения :spam и :ham в словаре и выводил класс большего значения из этих двух.

prediction = predict(m, "hello my name is kfung")
if prediction[:spam] > prediction[:ham]
    println("spam")
else
    println("ham")
end

Как мы и ожидали, результатом работы этой программы была строка “ham”, потому что предсказанное ею значение :ham было больше, чем значение :spam, следовательно, это, скорее всего, было неверным сообщением.

В конце концов, я завернул все в функцию, которая принимает строку в качестве аргумента, чтобы при вызове функции со строкой она выводила либо “spam”, если модель предсказывает это как спам-сообщение, либо “ham”, если модель предсказывает что это не спам-сообщение.

using CSV, DataFrames
using TextAnalysis: NaiveBayesClassifier, fit!, predict
function checkspam(msg::String)
    spamdata = DataFrame(CSV.read("spam.csv"; allowmissing=:none))
    m = NaiveBayesClassifier([:ham, :spam])
    for row in eachrow(spamdata)
        if row.v1 == "ham"
            fit!(m, filter(isvalid, row.v2), :ham)
        elseif row.v1 == "spam"
            fit!(m, filter(isvalid, row.v2), :spam)
        end
    end
    prediction = predict(m, msg)
    if prediction[:spam] > prediction[:ham]
        println("spam")
    else
        println("ham (not spam)")
    end
end

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

using CSV, DataFrames
using TextAnalysis: NaiveBayesClassifier, fit!, predict
spamdata = DataFrame(CSV.read("spam.csv"; allowmissing=:none))
global m = NaiveBayesClassifier([:ham, :spam])
for row in eachrow(spamdata)
    if row.v1 == "ham"
        fit!(m, filter(isvalid, row.v2), :ham)
    elseif row.v1 == "spam"
        fit!(m, filter(isvalid, row.v2), :spam)
    end
end
function checkspam(msg::String)
    prediction = predict(m, msg)
    if prediction[:spam] > prediction[:ham]
        println("spam")
    else
        println("ham (not spam)")
    end
end

В целом, я думаю, что это была очень успешная попытка создать новый спам-фильтр. Я узнал больше о том, как я могу применить Наивный Байес для моделирования независимых предположений для вероятностной классификации. В настоящее время наивный байесовский метод продолжает оставаться довольно популярным вариантом категоризации текста, примером которого является данный пример, и его можно противопоставить другим методам обучения моделей, таким как логистическая регрессия, при которой данные обучаются условно. У этой модели есть и другие приложения, такие как анализ настроений, на котором я скоро остановлюсь в другой статье, так что следите за обновлениями!

Большое спасибо за чтение!