Статьи

Машинное обучение: Руби и наивная теорема Байеса

Все мы слышали о тенденции «больших данных» в компьютерной науке: каждый и их бабушка пытаются получить представление о больших наборах данных. Часто инструментом для работы является машинное обучение. Большая часть сообщества Ruby вращается вокруг веб-разработки; даже здесь некоторые базовые знания в области машинного обучения могут пригодиться при творческом применении. Эта статья поможет вам освоиться с простой (но часто довольно эффективной) техникой машинного обучения: наивным байесовским классификатором.

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

Классификаторы

Прежде всего, что конкретно представляет собой «классификатор»? Концепция довольно проста. Учитывая некоторые описания (называемые «функциями») данного объекта, классификатор классифицирует этот объект в категорию. Очевидно, что это не идеальный процесс, и время от времени мы получаем объект, имеющий характеристики, которые могут вписаться в обе категории. Классификаторы обычно оцениваются по точности их классификаций.

Примерным примером классификации является обнаружение спама. С учетом фрагмента текста классификатор должен решить, является ли он спамом. Хороший вопрос, который следует задать: «Какие характеристики фрагмента текста определяют, является ли он спамом?». Эти характеристики или функции могут быть такими, как длина текста, наличие определенных слов или количество ссылок в электронном письме. Классификатор затем использует значения этих объектов (они являются либо логическими, либо действительными числами), чтобы выполнить классификацию.

Наивный байесовский классификатор и теорема Байеса

До сих пор мы рассматривали общие идеи, стоящие за классификатором, но не действительный механизм. Есть много способов на самом деле классифицировать вещи, каждый из которых имеет свои сильные и слабые стороны. Мы рассмотрим наивный байесовский классификатор (NBC) в этой статье.

Приведенные ниже примеры можно скопировать и вставить в текстовое поле на этой странице для более четкого рендеринга.

Допустим, у нас есть классы (т.е. категории) \ [y_1, y_2,…, y_k \], и нам дан набор наблюдений \ [X = x_1, x_2,…, x_n \]. NBC пытается найти значение :

\ [P (y_k \ vert X = X = x_1, x_2,…, x_n) \]

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

\ [P (A \ vert B) = \ frac {P (B \ vert A) \ cdot P (A)} {B} \]

На английском это говорит:

  • Чтобы найти вероятность события A (например, дождя), учитывая, что событие B происходит (например, облачное небо)
  • умножьте вероятность того, что B происходит, учитывая A (то есть облачное небо, учитывая, что идет дождь) и вероятность того, что A произойдет (то есть вероятность дождя)
  • разделить на вероятность B (то есть вероятность облачного неба).

Это немного сложно; чтение объяснения пару раз, пока оно не «сядет», может помочь (напишите вопросы в комментариях ниже!).

Мы можем использовать теорему Байеса, чтобы сказать:

\ [P (y_k \ vert X = x_1, x_2,…, x_n) =
\ frac {P (X = x_1, x_2, .. x_n \ vert y_k) P (y_k)} {P (x_1, x_2,… x_n)} \]

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

Мы до сих пор не знаем, как выяснить \ [P (X = x_1, x_2, .. x_n \ vert y_k) \] (что является вероятностью появления значений признаков, если они являются частью определенного класса.) Здесь мы делаем нечто, называемое «допущение независимости», которое гласит, что «для определенного класса значения характеристик не зависят друг от друга».

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

Как это происходит, мы можем записать это как наше выражение:

\ [P (y_k \ vert X = X = x_1, x_2,…, x_n) =
\ frac {P (x_1 \ vert y_k) P (x_2 \ vert y_k)… \ cdot P (x_k \ vert y_k) P (y_k)} {P (x_1, x_2,… x_n)} \]

Мы можем думать об этой вероятности как о счете. Как только наш NBC просматривает значения функций, он присваивает каждому классу баллы, а самый высокий — правильная классификация значений функций. Но знаменатель выражения не имеет ничего общего с классом \ [y_k \] и поэтому является постоянным для всех классов, поэтому мы можем просто избавиться от него. Наше окончательное выражение может быть записано как:

\ [\ text {Оценка класса для} y_k = P (x_1 \ vert y_k) P (x_2 \ vert y_k)… \ cdot P (x_k \ vert y_k) P (y_k) \]

Итак, мы разбили проблему до такой степени, что нам нужно знать только два типа вещей: вероятность того, что данное значение функции является частью класса \ [P (x_i \ vert y_k) \] и вероятность класс, встречающийся \ [P (y_k) \]

Последнее можно сделать довольно простым, если предполагается, что классы примерно одинакового размера:

\ [P (y_k) = \ frac {1} {k} \]

Второй немного сложнее. К счастью, этот парень из 18-го века по имени Гаусс понял это для нас (используя нормальный дистрибутив, который не работает везде, но должен во многих случаях):

\ [P (x_i | y_k) = \ frac {1} {\ sqrt {2 \ pi \ sigma ^ 2_c}} \, e ^ {- \ frac {(v- \ mu_c) ^ 2} {2 \ sigma ^ 2_c}} \]

В вышеприведенном выражении \ [\ sigma_c \] представляет стандартное отклонение i-го признака класса \ [y_k \], а \ [\ mu_c \] представляет среднее его значения. Если это уравнение выглядит как mumbo jumbo, единственное, что вам действительно нужно понять из этого, — это то, что оно использует идею, что значения, далекие от среднего значения, имеют меньшую вероятность появления (или, точнее, используют нормальное распределение).

Достаточно математики для времени; давайте доберемся до Руби.

Реализация

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

{
  "class1" => [
    [feature1, feature2, feature3, ..., featureN],
    [feature1, feature2, feature3, ..., featureN]
  ],

  "class2" => [
    [feature1, feature2, feature3, ..., featureN],
    [feature1, feature2, feature3, ..., featureN]
  ],
}

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

Мы можем начать с набросков некоторых основных методов:

 #used for mean, standard deviation, etc.
require 'descriptive_statistics'

class NaiveBayes

  #training data format:
  #{class-name: [[parameter1, parameter2, parameter3, ...],
  #              [parameter1, parameter2, parameter3,...]}
  def initialize(training_data, dim)
    @training_data = training_data
    @dimension = dim

  end

  def num_classes
    return @training_data.length
  end

  def classes
    return @training_data.keys
  end

Обратите внимание, что @dimension Теперь, когда у нас есть несколько служебных методов, мы можем реально взломать сущность алгоритма NBC. Чтобы вычислить \ [P (x_i \ vert y_k) \], мы должны вычислить среднее и стандартное отклонение вхождений объекта.

Вот что я имею в виду. Если у нас есть следующий набор тренировок:

 {
  ...
  "class1" => [
  [1, 1, 2],
  [1, 2, 2],
  [1, 16, 2],
  [1, 4, 2]
  ]
}

Тогда стандартное отклонение для первого и последнего признака (который не изменяется) будет равно 0, тогда как для второго признака тем же показателем является положительное число. Для того, чтобы вычислить эту информацию, нам нужен метод, который может поместить все функции определенного индекса в данном классе в один набор. Просите, и вы получите:

 def feature_set(index, class_name)
  feature_set = []
  training_set = @training_data[class_name]

  training_set.length.times do |i|
    feature_set << training_set[i][index]
  end

  return feature_set
end

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

Теперь мы можем собрать код для \ [P (x_i \ vert y_k) \]

 #given a class, this method determines the probability
#of a certain value ocurring for a given feature
#index: index of the feature in consideration in the training data
#value: the value of the feature for which we are finding the probability
#class_name: name of the class in consideration
def feature_probability(index, value, class_name)
  #get the feature value set
  fs = feature_set(index, class_name)

  #statistical properties of the feature set
  fs_std = fs.standard_deviation
  fs_mean = fs.mean
  fs_var = fs.variance

  #deal with the edge case of a 0 standard deviation
  if fs_std == 0
    return fs_mean == value ? 1.0 : 0.0
  end

  #calculate the gaussian probability
  pi = Math::PI
  e = Math::E

  exp = -((value - fs_mean)**2)/(2*fs_var)
  prob = (1.0/(Math.sqrt(2*pi*fs_var))) * (e**exp)

  return prob
end

Возвращаясь к нашему окончательному выражению для оценки класса:

\ [\ text {Оценка класса для} y_k = P (x_1 \ vert y_k) P (x_2 \ vert y_k)… \ cdot P (x_k \ vert y_k) P (y_k) \]

Нам нужно умножить вместе \ [P (x_i \ vert y_k) \] для всех индексов, окончательно умножив это на \ [P (y_k) \], чтобы получить результат. Руби облегчает:

 #multiply together the feature probabilities for all of the 
#features in a class for given values
def feature_mult(feature_values, class_name)
  res = 1.0
  feature_values.length.times do |i|
    res *= feature_probability(i, feature_values[i], class_name)
  end

  return res
end

#this is where we compute the final naive Bayesian probability
#for a given set of features being a part of a given class.
def class_probability(feature_values, class_name)
  class_fraction = 1.0 / num_classes
  feature_bayes = feature_mult(feature_values, class_name)
  res = feature_bayes * class_fraction
  return res
end

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

 #This the method we should be calling!
#Given a set of feature values, it decides
#what class to categorize them under
def classify(feature_values)
  res = classes.sort_by do |class_name|
    class_probability(feature_values, class_name)
  end

  return res[-1]
end

Код для всех этих частей очень прост (за исключением, возможно, функции feature_probability Блоки, хороший синтаксис цикла, простая сортировка с помощью блока клавиш и т. Д. Делают Ruby очень приятным языком для машинного обучения.

Тест-драйв

Теперь, когда мы собрали класс NaiveBayes, мы можем проверить его. Набор данных Iris можно считать стандартом в сообществе машинного обучения. Он предоставляет данные о размерах лепестков и чашелистиков для определенных цветов ириса и маркировку вида для каждой записи.

Примером записи может быть:

 4.8,3.0,1.4,0.3,Iris-setosa

Наша цель состоит в том, чтобы научить наш наивный байесовский классификатор принимать значения признаков (числа) и сообщать нам, к какому виду видов могут быть эти числа. По сути, мы просто должны собрать набор обучающих данных и проверить классификатор с отдельным файлом записей, которые не являются частью обучающего набора. Вы можете найти и код, и данные в репозитории этой статьи. Как это бывает, NBC может правильно определить 11 из 12 значений функций, что довольно неплохо, учитывая небольшой тренировочный набор.

Завершение

Переход к языку для MLers в наши дни, кажется, Python. Отчасти это может быть связано с тем, что Python имеет обширную библиотечную поддержку в форме scikit-learn и numpy, а также потому, что веб-сообщество Ruby не очень активно занимается машинным обучением. Я думаю, что Ruby — это хороший язык для реализации алгоритмов машинного обучения (особенно простых), и я также считаю, что веб-разработчикам также было бы неплохо освоить некоторые приемы ML!