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

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

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

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

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

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

Уравнение Маркова можно упростить, вероятно, следующим образом:

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

Код

#### downloading spam dataset #####
!wget https://lazyprogrammer.me/course_files/spam.csv

from sklearn.model_selection import train_test_split
import numpy as np
import pandas as pd

spam_df = pd.read_csv('spam.csv', encoding = "ISO-8859-1")

spam_df = spam_df.drop(['Unnamed: 2', 'Unnamed: 3', 'Unnamed: 4'], axis = 1)
# mapping the category names to labels #
map_dict = {'ham': 0, 'spam': 1}
spam_df['labels'] = spam_df['v1'].map(map_dict)

input_text = spam_df['v2']
label = spam_df['labels']

# splitting the data into test and train #

train_text, test_text, Ytrain, Ytest = train_test_split(input_text, label)

##### starting point for word indexing #####
##### we are setting aside 0 for unknown values, i.e. the tokens that are present in 
##### test but not in train. 

idx = 1
word2idx = {'<unk>': 0}

# populate word2idx
for text in train_text:
    tokens = text.split()
    for token in tokens:
      if token not in word2idx:
        word2idx[token] = idx
        idx += 1


# convert data into integer format
train_text_int = []
test_text_int = []

for text in train_text:
  tokens = text.split()
  line_as_int = [word2idx[token] for token in tokens]
  train_text_int.append(line_as_int)

for text in test_text:
  tokens = text.split()
  line_as_int = [word2idx.get(token, 0) for token in tokens]
  test_text_int.append(line_as_int)
# initialize A and pi matrices - for both classes. The number of A and pi matrices depends on the number of classes or categories we have #
V = len(word2idx)

A0 = np.ones((V, V))
pi0 = np.ones(V)

A1 = np.ones((V, V))
pi1 = np.ones(V)

# compute counts for A and pi

def compute_counts(text_as_int, A, pi):
  for tokens in text_as_int:
    last_idx = None
    for idx in tokens:
      if last_idx is None:
        # it's the first word in a sentence
        pi[idx] += 1
      else:
        # the last word exists, so count a transition
        A[last_idx, idx] += 1

      # update last idx
      last_idx = idx


compute_counts([t for t, y in zip(train_text_int, Ytrain) if y == 0], A0, pi0)
compute_counts([t for t, y in zip(train_text_int, Ytrain) if y == 1], A1, pi1)



# normalize A and pi so they are valid probability matrices

A0 /= A0.sum(axis=1, keepdims=True)
pi0 /= pi0.sum()

A1 /= A1.sum(axis=1, keepdims=True)
pi1 /= pi1.sum()

# log A and pi since we don't need the actual probs
logA0 = np.log(A0)
logpi0 = np.log(pi0)

logA1 = np.log(A1)
logpi1 = np.log(pi1)

# compute priors for both categories #


count0 = sum(y == 0 for y in Ytrain)
count1 = sum(y == 1 for y in Ytrain)
total = len(Ytrain)
p0 = count0 / total
p1 = count1 / total
logp0 = np.log(p0)
logp1 = np.log(p1)
p0, p1

Теперь, когда у нас есть полная матрица A и пи, мы можем приступить к созданию классификатора.

# build a classifier
class Classifier:
  def __init__(self, logAs, logpis, logpriors):
    self.logAs = logAs
    self.logpis = logpis
    self.logpriors = logpriors
    self.K = len(logpriors) # number of classes

  def _compute_log_likelihood(self, input_, class_):
    logA = self.logAs[class_]
    logpi = self.logpis[class_]

    last_idx = None
    logprob = 0
    for idx in input_:
      if last_idx is None:
        # it's the first token
        logprob += logpi[idx]
      else:
        logprob += logA[last_idx, idx]
      
      # update last_idx
      last_idx = idx
    
    return logprob
  
  def predict(self, inputs):
    predictions = np.zeros(len(inputs))
    for i, input_ in enumerate(inputs):
      posteriors = [self._compute_log_likelihood(input_, c) + self.logpriors[c] \
             for c in range(self.K)]
      pred = np.argmax(posteriors)
      predictions[i] = pred
    return predictions


# each array must be in order since classes are assumed to index these lists
clf = Classifier([logA0, logA1], [logpi0, logpi1], [logp0, logp1])

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

## training data ##

Ptrain = clf.predict(train_text_int)
print(f"Train acc: {np.mean(Ptrain == Ytrain)}")
Train acc: 0.99
Ptest = clf.predict(test_text_int)
print(f"Test acc: {np.mean(Ptest == Ytest)}")
Test acc: 0.95

Несмотря на то, что точность как для поезда, так и для теста одинакова, у нас несбалансированный набор данных, и, следовательно, оценка F1 будет более полезной метрикой для принятия во внимание, чем точность.

from sklearn.metrics import f1_score

f1_score(Ytrain, Ptrain)

ans: 0.99
f1_score(Ytest, Ptest)

ans: 0.82

Это дает нам лучшее представление о том, как работает наша модель. Даже с таким простым предположением мы можем получить высокий балл F1 на наших тестовых данных.