В този пример използваме набор от имейли или документи, които са написани от две различни лица. Целта е да се обучи модел на Naive Bayes, за да може да предвиди кой е написал документ/имейл, като се имат предвид думите, използвани в него

Хранилището на Github с файловете, използвани в този пример, може да бъде намерено тук. Файлът nb_email_author.py съдържа скрипта, който зарежда данните, обучава модела и намира резултата на прогнозата за обучаващи и тестови набори.

Тук обясняваме основните части на скрипта и резултатите.

Зареждане на данните

Първият раздел на скрипта зарежда данните. Единствената особеност тук е, че „туршията“ се използва за десериализация на оригиналния файл с данни (за повече информация относно киселинността и сериализацията/десериализацията, погледнете тази „връзка“).

Зареждат се два файла, един, който съдържа имейлите или документите, и друг само с авторите на имейлите. След като десериализираме и заредим файловете, имаме два масива (списъци), един с име words и един с име authors, всеки с размер 17 578. Всеки елемент в words е единичен низ, който съдържа имейл или документ. Всеки елемент в `authors` е 0 или 1.

Както обикновено, ние разделяме данните на обучителни и тестови набори, използвайки метода Scikit-learn sklearn.model_selection.train_test_split.

from sklearn.model_selection import train_test_split
features_train, features_test, labels_train, labels_test = train_test_split(words, authors, test_size=0.1, random_state=10)

Векторизация на текст

Когато се работи с текстове в машинното обучение, е доста обичайно да се трансформира текстът в данни, които могат лесно да бъдат анализирани и количествено определени.

За това най-често използваната техника е tf-idf съкращение за „термин честота-обратна честота на документа“, което основно отразява колко важна е дадена дума за документ (имейл) в колекция или корпус (нашият набор от имейли или документи).

tf-idf е статистика, която се увеличава с броя на появяванията на дадена дума в документа, санкционирана от броя на документите в корпуса, които съдържат думата (Wikipedia).

За наше щастие, Scikit-learn има метод, който прави точно това (sklearn.feature_extraction.text.TfidfVectorizer). Вижте документацията тук).

Така че прилагаме този метод към нашите данни по следния начин:

from sklearn.feature_extraction.text import TfidfVectorizer
vectorizer = TfidfVectorizer(sublinear_tf=True, max_df=0.5, stop_words='english')
features_train = vectorizer.fit_transform(features_train)
features_test = vectorizer.transform(features_test)

TfidfVectorizer настройва векторизатора. Тук променяме sublinear_tf на true, което замества tf с 1 + log(tf). Това разглежда въпроса, че „двадесет срещания на термин в документ“ не представляват „двадесет пъти по-голяма значимост от едно единствено срещане“ („връзка“). Следователно намалява важността на думите с висока честота (обърнете внимание, че 1+log(1) = 1,докато1+log(20) = 2,3).

Освен това, stop_words е зададено на „английски“, така че спиращите думи като „и“, „той“, „той“ ще бъдат игнорирани в този случай, а max_df=0.5 означава, че игнорираме термини, които имат честота на документа по-висока от 0,5 ( т.е. делът на документите, в които се среща терминът).

След това приспособяваме и трансформираме функциите (термини или думи в нашия случай) както за обучаващи, така и за тестови комплекти. Забележете, че за набора от влакове използваме fit_transform, а за тестовия набор използваме само transform.

Това има смисъл, тъй като искаме моделът да научи речника и честотите на документите от комплекта влакове и след това да трансформира характеристиките на влака в матрица термини-документ. За тестовия набор ние просто искаме да използваме научените честоти на документа (idf) и речника, за да го трансформираме в матрица термин-документ.

Нека да видим как изглежда това с опростен пример:

Да предположим, че имаме следния корпус на влака, отново всеки елемент представлява един документ/имейл:

corpus = [
    "This is my first email.",
    "I'm trying to learn machine learning.",
    "This is the second email",
    "Learning is fun"
]

Сега нека го напаснем и трансформираме:

vectorizer = TfidfVectorizer()
X = vectorizer.fit_transform(corpus)
print(X.__str__)
# <4x13 sparse matrix of type ‘<class ‘numpy.float64’>’ with 18 stored elements in Compressed Sparse Row format>

fit_transform връща разредена матрица:

print(X)
# (0, 10)   0.41263976171812644
# (0, 3)    0.3340674500232949
# (0, 7)    0.5233812152405496
# (0, 1)    0.5233812152405496
# (0, 0)    0.41263976171812644
# (1, 12)   0.4651619335222394
# (1, 11)   0.4651619335222394
# (1, 4)    0.4651619335222394
# (1, 6)    0.4651619335222394
# (1, 5)    0.3667390112974172
# (2, 10)   0.41263976171812644
# (2, 3)    0.3340674500232949
# (2, 0)    0.41263976171812644
# (2, 9)    0.5233812152405496
# (2, 8)    0.5233812152405496
# (3, 3)    0.4480997313625986
# (3, 5)    0.5534923152870045
# (3, 2)    0.7020348194149619

Ако преобразуваме X в 2D масив, изглежда така (има общо 13 колони, всяка представлява дума/термин, нечетните колони са пропуснати за краткост):

vocabulary = vectorizer.get_feature_names()
pd.DataFrame(data=X.toarray(), columns=vocabulary).iloc[:,0::2]

print(vocabulary)
# [‘email’, ‘first’, ‘fun’, ‘is’, ‘learn’, ‘learning’, ‘machine’, ‘my’, ‘second’, ‘the’, ‘this’, ‘to’, ‘trying’]

Сега да предположим, че имаме следния „тестов“ документ:

test = [“I’m also trying to learn python”]

Нека го трансформираме и да го разгледаме:

X_test = vectorizer.transform(test)
pd.DataFrame(data=X_test.toarray(), columns=vocabulary).iloc[:, 0::2]

Ето го, така текстовете или документите се векторизират за по-нататъшен анализ.

Избиране на по-малък набор от функции

Въпреки че избирането на по-малък набор от функции не е строго необходимо, може да бъде изчислително предизвикателство да се обучи модел с твърде много думи или функции.

В този пример използваме SelectPercentile на Scikit-learn, за да изберем функции с най-високи резултати („документация“):

from sklearn.feature_selection import SelectPercentile, f_classif
selector = SelectPercentile(f_classif, percentile=10)
selector.fit(features_train, labels_train)
features_train = selector.transform(features_train).toarray()
features_test = selector.transform(features_test).toarray()

Селекторът използва f_classif като функция за оценка, която изчислява ANOVA F-стойностите за извадката. По принцип ние избираме термините с най-големи F-стойности (т.е. термини или думи, за които средната стойност на честотата е най-вероятно да е различна в различните класове или автори). Това е обичайно, за да се изберат най-добрите разграничителни характеристики в класовете (от първоначално 38 209 думи, в крайна сметка имаме 3 821).

Обучение на наивен бейс модел

За този пример използваме реализация на Gaussian Naive Bayes (NB) (документация на Scikit-learn тук). В бъдещи статии ще обсъдим подробно теорията зад Naive Bayes.

Засега си струва да се каже, че NB се основава на прилагане на правилото на Bayes за изчисляване на вероятността или вероятността набор от думи (документ/имейл) да е написан от някой или някакъв клас (напр. P(„Крис“ | „научавам“, „машина“, „опитвам“,…)).

Въпреки това, няма такова нещо като наивно правило на Бейс. Терминът „наивен“ възниква поради предположението, че функциите са независими една от друга (условна независимост), което означава, за нашия анализ на имейли, че приемаме, че местоположението на думите в изречение е напълно произволно (т.е. „съм“ или „робот“ е еднакво вероятно да последва думата „аз“, което разбира се не е вярно).

Забележка със Scikit-learn

Като цяло обучението на модели за машинно обучение със Scikit-learn е лесно и обикновено следва същия модел:

  • инициализиране на екземпляр на модела на класа,
  • отговаря на данните за влака,
  • прогнозиране на тестовите данни (пропускаме това тук),
  • изчисляване на резултати както за тренировъчни, така и за тестови набори.
from sklearn.naive_bayes import GaussianNB
from time import time
t0 = time()
model = GaussianNB()
model.fit(features_train, labels_train)
print(f”\nTraining time: {round(time()-t0, 3)}s”)
t0 = time()
score_train = model.score(features_train, labels_train)
print(f”Prediction time (train): {round(time()-t0, 3)}s”)
t0 = time()
score_test = model.score(features_test, labels_test)
print(f”Prediction time (test): {round(time()-t0, 3)}s”)
print(“\nTrain set score:”, score_train)
print(“Test set score:”, score_test)

Резултатите са следните:

>>> Training time: 1.601s
>>> Prediction time (train): 1.787s
>>> Prediction time (test): 0.151s
>>> Train set score: 0.9785082174462706
>>> Test set score: 0.9783845278725825

Не е зле!