SPAM or HAM
Введение (Introduction)
В цифровое время очень важно фильтровать информацию которая приходит к вам на электронную почту или SMS. Часто бывает такое что почты пользователей превращаются в мусор из рекламы, скама и бесполезных объявлении. Эта статья о том как я написал модель на основе NLP* которая определяет является ли полученная смска спамом.
Я постараюсь быть краток и не выходить за грани инструментов и методов которые я буду использовать.
NLP (Natural Language Processing) — это обработка естественного языка, область искусственного интеллекта, которая занимается взаимодействием компьютеров и людей с использованием естественного языка. Цель NLP — позволить компьютерам понимать, интерпретировать и генерировать человеческий язык таким образом, чтобы они могли выполнять задачи, связанные с обработкой текстов или речи.
Постановка задачи
Любая машинная модель начинается с набора данных (или датасет). Очень важно выбрать корректный датасет, потому что именно она будет служить основой нашей модели. В моем случае, я выбрал датасет под названием 'SMS Spam Collection Dataset'. Подробнее: https://www.kaggle.com/datasets/uciml/sms-spam-collection-dataset
Описание данных:
Датасет представляет собой коллекцию SMS-сообщении которые классифицированы как ham и spam, то есть не спам и спам соответсвенно.
Датасет состоит из двух колон которые я позже переименую в:
Пример:
ham - "Nah I don't think he goes to usf, he lives around here though"
spam - "WINNER!! As a valued network customer you have been selected to receivea 900 prize reward!"
Бинарная классификация — это тип задачи в области машинного обучения и статистики, при которой требуется разделить данные на две категории или класса. В такой задаче модель обучается на наборе данных, где каждый пример помечен как принадлежащий к одному из двух классов.
Гипотезы
Первая:
Прежде чем мы начнем работать с текстом давайте подумаем над тем что может повлиять на само разделение между двумя видами текста. В глаза бросается длинна текста: spam сообщения гораздо длиннее чем ham.
Немного поанализировав, мы можем выяснить влияние длинны текста. Для этого создаем новую колону 'len' чтоб разместить там длину текста на каждой строке:
df['len'] = df['text'].apply(lambda x: len(x) - x.count(' '))Теперь посмотрим как это выглядит на гистограмм.
bins = np.linspace(0, 150, 50) plt.hist(df[df['label'] == 'spam']['len'], bins, alpha=0.5, label='spam') plt.hist(df[df['label'] == 'ham']['len'], bins, alpha=0.3, label='ham') plt.legend() plt.show()
По графику можно сказать что длинна сообщения действительно играет большую роль в определении типа текста. Так как spam текст сильно смещен направо в то время как ham налево. Вывод: есть необходимость использовать длину сообщения как один из параметров входных данных.
Вторая:
Второе что так же может повлиять на определение это пунктуация. Как влияет частота пунктуации используемое в тексте. Проведем те же операции:
def count_punct(text):
count = sum([1 for char in text if char in string.punctuation])
return round(count / (len(text) - text.count(' ')), 3) * 100
df['punct%'] = df['text'].apply(lambda x: count_punct(x))
bins = np.linspace(0, 40, 50)
plt.hist(df[df['label'] == 'spam']['punct%'], bins, alpha=0.5, label='spam')
plt.hist(df[df['label'] == 'ham']['punct%'], bins, alpha=0.3, label='ham')
plt.legend()
plt.show()
В этом случае все наоборот. Смещение примерно одинаковое, значит частота пунктуации не поможет определить является ли текст спамом. Более простыми словами: этот график говорит нам о том что и спам текст и не спам текст использует пунктуацию на одинаковой частоте.
Предобработка данных (Data Preprocessing)
Шаг 1:
"I am Batman." и "I am batman!" это не одно и то же предложение в глазах нашего алгоритма. Следовательно, для дальнейшей работы с текстом нам нужно полностью очистить ее от пунктуации и переписать все в нижний регистр. Тогда мы получаем простой, но понятный и удобный для работы "i am batman".
Шаг 2:
Не все слова несут за собой полезную информацию для обработки. Такие слова называются stopwords. Пример: I, me, am, are, hadn't, where и тд. То есть из предложения "i am batman", мы убираем слова I и am так как чтобы понять о чем предложение, нам буквально хватает одного слова "batman". Давайте рассмотрим еще один пример: "Can writing article be boring?" превращается в "writing article boring". Для человека, очевидно, полностью теряется контекст, но теперь наш алгоритм понимает что "Can writing article be boring?" имеет много общего с например "Writing an article can be boring if the topic isn't interesting".
Шаг 3:
Stemming и токенизация. Стемминг просто напросто упрощает слова. В частности он убирает окончания "-ing", "-ed", "-tion" и тд. Потому что нам при работе с текстом нет дела до грамматики и подобных окончании. Мы хотим чтобы слова такие как "interesting", "interested" в работе были одним единым словом "interest". А токенизация просто распределит предложение в массив из слов.
Сложное и непонятное предложение как "Writing an article can be boring if the topic isn't interesting." превращается в удобное для работы массив из слов ['writ', 'articl', 'bor', 'topic', 'interest']
stopwords = nltk.corpus.stopwords.words('english')
ps = nltk.PorterStemmer()
def clean_text(text):
text = ''.join([word.lower() for word in text if word not in string.punctuation])
tokens = re.split('\W+', text)
text = [ps.stem(word) for word in tokens if word not in stopwords]
return textПоследний шаг:
Последний шаг представляет собой превращение слов в числа, но каким образом. Для нашей задачи я буду использовать векторизацию TFIDF.
TF-IDF (Term Frequency-Inverse Document Frequency) — это популярная методика для представления текста в виде числовых признаков в задачах обработки естественного языка (NLP). Она помогает оценить важность слов в документе с учетом того, насколько часто слово встречается в других документах.
Данная векторизация распределяет все слова в большую матрицу где колоны это все используемы слова, а строки это наши предложения. А значения это степень важности слова в предложении.
tfidf_vect = TfidfVectorizer(analyzer=clean_text) X_tfidf = tfidf_vect.fit_transform(df['text']) X = pd.concat([df['len'], df['punct%'], pd.DataFrame(X_tfidf.toarray())], axis=1)
Таблица выше это то что стало с нашей колонкой "text". Мы будем использовать эту таблицу как параметры X. Как если бы мы писали функцию y = f(X). То колона "label" будет y, а f это наша модель о которой мы поговорим в следующем этапе.
Давайте попробуем разбить все выше упомянутые предложения в таблицу TFIDF Vectorisation:
from sklearn.feature_extraction.text import TfidfVectorizer
documents = [
"batman",
"writ articl bor",
"writ articl bor topic interest"
]
tfidf_vectorizer = TfidfVectorizer()
tfidf_matrix = tfidf_vectorizer.fit_transform(documents)
tfidf_df = pd.DataFrame(tfidf_matrix.toarray(), columns=tfidf_vectorizer.get_feature_names_out())
tfidf_dfОбучение модели (Model Training)
Для обучения, я буду использовать Random Forest Classifier*. Основные преимущества для данной задачи в том что оно может обрабатывать большие наборы данных и эффективно использовать признаки. Но что более важно это то что спам тексты составляют всего 13% от общего количества. В классификации классы дисбалансированы (меньшее количество спам-сообщений по сравнению с обычными). Random Forest может компенсировать этот дисбаланс за счет выбора случайных подмножеств данных и голосования.
Random Forest Classifier — это алгоритм машинного обучения для решения задач классификации и регрессии, основанный на ансамбле решений. Он состоит из множества решающих деревьев, работающих вместе для повышения точности предсказаний. Основная идея Random Forest заключается в том, чтобы создать "лес" из нескольких деревьев решений, где каждое дерево обучается на случайном подмножестве данных и признаков. Итоговый прогноз делается на основе голосования всех деревьев.
Шаг 1:
Разделяем данные на тестовый и тренировочный набор в соотношении 20/80. Будем тренировать на тренировочном наборе, затем проверять и оценивать на тестовом.
X_train, X_test, y_train, y_test = train_test_split(X, df['label'], test_size=0.2)
Шаг 2:
Инициализируем модель и загружаем наши данные.
rf = RandomForestClassifier(n_estimators=100) rf_model = rf.fit(X_train, y_train)
Random Forest имеет такую функцию как feature_importances_, которая показывает степень важности параметра в обучении. Давайте рассмотрим самые полезные из них
sorted(zip(rf_model.feature_importances_, X_train.columns), reverse=True)[:10]
Результат:
[(0.05284560707873848, '8038'),
(0.038103717348420954, 'len'),
(0.03153365424945696, '1789'),
(0.02343684600283975, '7292'),
(0.021851042514375484, '4765'),
(0.02169782852359817, '3118'),
(0.017913250216369424, '2018'),
(0.015055476550308528, '6695'),
(0.014056416317266856, '7402'),
(0.012347618727250731, '5945')]
Значение 'len' находится в топ 2, значит наша гипотеза и анализ были верны.
Шаг 3:
from sklearn.metrics import precision_recall_fscore_support as score
y_pred = rf_model.predict(X_test)
precision, recall, fscore, support = score(y_test, y_pred, pos_label='spam', average='binary')
print(f'Precision: {precision}\nRecall: {recall}\nFscore: {fscore}\nAccuracy: {(y_pred==y_test).sum() / len(y_pred)}')Результат:
Precision*: 1.0
Recall*: 0.8282208588957055
Fscore*: 0.9060402684563758
Accuracy*: 0.9748878923766816
Precision score (точность) — это метрика, используемая для оценки качества модели классификации. Precision показывает, какую долю объектов, предсказанных как положительные(spam), модель действительно классифицировала правильно.
Recall (или полнота) — это метрика, используемая для оценки качества модели машинного обучения, особенно в задачах классификации. Она показывает, как хорошо модель находит все положительные примеры (spam) из всех возможных.
F-score (или F1-score) — это метрика качества модели, которая учитывает как точность (precision), так и полноту(recall). Она особенно полезна в задачах, где классы несбалансированы как при классификации спам/не спам.
Accuracy score (точность) — это метрика оценки качества модели классификации, которая показывает, какую долю правильных предсказаний модель сделала от общего числа предсказаний. Она используется для измерения производительности модели.
Заключение (Conclusion)
Реальный код продолжается далее уже с использованием других моделей с разными параметрами, но думаю на этом с вас хватит. Спасибо всем кто читал данную статейку.