Кажется, что все разные языки программирования лучше подходят для задач машинного обучения, чем Ruby, верно? В Python есть scikit-learn , в Java есть Weka , и есть Shogun для машинного обучения на C ++, и это лишь некоторые из них. С другой стороны, Ruby имеет отличную репутацию быстрого прототипирования.
Итак, почему бы вам не создать прототип системы машинного обучения с Ruby? Вызов принят! В этом руководстве мы создадим систему, которая может автоматически классифицировать спортивные статьи BBC для вас.
О, и мы сделаем это в Ruby, хорошо? Что ж, это не совсем так — мы будем использовать JRuby и библиотеку Java Weka через драгоценный камень weka .
подготовка
Сначала установите JRuby v9.0.0.0 +. Затем создайте каталог ml_with_jruby
и поместите в него следующий Gemfile:
source 'https://rubygems.org' # use your JRuby version here ruby '2.3.1', engine: 'jruby', engine_version: '9.1.5.0' gem 'weka' # this provides us with the weka lib gem 'scalpel' # used for text processing
В вашей среде JRuby запустите bundle install
для установки гемов.
Затем загрузите бесплатный набор данных спортивных статей BBC и переместите распакованные каталоги статей в каталог ./data/training
.
Наконец, переместите последние две статьи каждого вида спорта в отдельный каталог ./data/test
.
Структура вашего проекта должна выглядеть так:
└── ml_with_jruby ├── data │ ├── test │ │ ├── athletics │ │ ├── cricket │ │ ├── football │ │ ├── rugby │ │ └── tennis │ └── training │ ├── athletics │ ├── cricket │ ├── football │ ├── rugby │ └── tennis └── Gemfile
Тексты в каталоге test
будут нашими тестовыми файлами и будут классифицироваться с помощью нашего trained
классификатора.
Подождите … обучение, классификация, классификатор? Здесь много терминов. Давайте кратко рассмотрим, что они имеют в виду.
Что такое классификация?
Классификация означает «маркировку данных». Например, статья может быть помечена как теннис или крикет . Эти ярлыки, теннис и крикет , называются классами . Алгоритм, который выбирает метку для одной из наших статей, называется классификатором .
В настоящее время существуют различные типы проблем классификации: контролируемые и неконтролируемые . Первый часто называют «кластеризацией», когда у вас нет примеров данных, и вы заранее не знаете, на какие классы ваш алгоритм будет разбивать ваши данные.
Контролируемый означает, что у нас есть предварительно помеченные данные, такие как наши помеченные статьи. Мы используем эти помеченные статьи для обучения нашего классификатора или, другими словами: для построения модели, которая может принять решение о том, как классифицировать новые данные. После тренинга мы можем передать немаркированные статьи нашему классификатору, и он даст нам ярлык для каждого из них.
Тем не менее, три шага для создания системы контролируемой классификации:
- Создание учебного набора данных из необработанных данных
- Обучение классификатора с помощью набора данных обучения
- Классификация новых данных с помощью обученного классификатора
Создание набора данных
Давайте начнем с компиляции наших тренировочных данных.
Нам нужны примеры данных, чтобы сообщить нашему классификатору, как выглядят различные типы статей. Компьютеры умные, но мы не можем ожидать, что они берут текст и имеют хорошее представление о том, что это за спорт. Итак, первым шагом является преобразование нашего необработанного текста в представление, с которым может работать наш классификатор. Компьютер должен хорошо разбираться с числами, поэтому мы будем использовать набор чисел, которые описывают свойства текста (так называемые функции ).
Мы должны найти некоторые функции, которые могут лучше всего разделить наши данные на различные типы статей. Мы могли бы вычислить, например, общую длину текста или количество определенных ключевых слов в тексте. На этом этапе вы можете проявить творческий подход, выбирая то, что приходит вам на ум и имеет смысл. Существуют комбинации функций, которые хорошо работают вместе, в то время как другие снижают производительность классификатора. Получив пул функций, вы можете использовать алгоритмы для выбора наиболее ценных функций. Для простоты мы не будем рассматривать выбор функций в этом учебном пособии, а будем просто использовать наш здравый смысл для выбора небольшого набора функций.
Извлечение функций из текста
Мы сделаем извлечение FeatureExtractor
классе FeatureExtractor
который берет фрагмент текста и возвращает хэш свойств и их числовые представления. Давайте непосредственно обработаем данный текст в абзацы, предложения (используя скальпель ) и слова. Они нам понадобятся достаточно скоро, когда мы добавим наши функции.
# feature_extractor.rb require 'scalpel' class FeatureExtractor attr_reader :text, :paragraphs, :sentences, :words def initialize(text) @text = text.strip @paragraphs = text.split(/\n{2,}/) @sentences = Scalpel.cut(text) @words = text.scan(/[\w'-]+/) end def features {} # to be implemented next 🙂 end end
Во-первых, мы добавим некоторые очевидные особенности: подсчитайте появление слов, описывающих сам вид спорта, таких как «теннис» в статье о теннисе, «крикет» в статье о крикете и т. Д. (Обратите внимание, что, например, «спортсмен» имеет значение « спортсмены », а также« атлетик »и т. д.).
class FeatureExtractor # ... def features { athletics_hints_count: match_count('athlet'), cricket_hints_count: match_count('cricket'), football_hints_count: match_count('football'), rugby_hints_count: match_count('rugby'), tennis_hints_count: match_count('tennis') } end private def match_count(word) text.scan(/#{word}/i).count end end
Может быть интересно, сколько имен собственных, таких как имена и команды, появляются в тексте. Поэтому мы добавим функцию capitalized_words_count
.
Статьи, например, о теннисе и легкой атлетике, с большей вероятностью будут говорить о женщинах, чем, например, статьи о футболе. Таким образом, мы рассмотрим это в функции, которая сканирует мужские и женские ключевые слова и говорит, которые появляются чаще всего. Давайте назовем это gender_dominance
.
Кроме того, добавьте некоторые более общие текстовые функции, такие как text_length
, text_length
, words_per_sentence_average
и words_per_sentence_average
.
Когда вы читаете пару наших учебных статей, кажется, что некоторые люди должны сказать больше, чем другие, поэтому давайте посчитаем цитаты в тексте.
Вы поняли идею. Просто попробуйте извлечь некоторые свойства, которые, вероятно, могут отличить содержание одной статьи от другой.
Мы добавим некоторые дополнительные функции, которые указывают, является ли это больше командным или индивидуальным видом спорта, путем подсчета количества подсказок, таких как местоимения (я, мой, я против нас, наш, нас), и функции number_count
которая может указывать виды спорта, где очки или времена важны.
С этим у нас все хорошо, и мы можем закончить наш класс экстрактора:
class FeatureExtractor # ... def features { athletics_hints_count: match_count('athlet'), cricket_hints_count: match_count('cricket'), football_hints_count: match_count('football'), rugby_hints_count: match_count('rugby'), tennis_hints_count: match_count('tennis'), capitalized_words_count: capitalized_words_count, gender_dominance: gender_dominance, text_length: text.length, sentences_count: sentences.count, paragraphs_count: paragraphs.count, words_per_sentence_average: words_per_sentence_average, quote_count: quote_count, single_sport_hints_count: terms_count(%w(I me my)), team_sport_hints_count: terms_count(%w(we us our team)), number_count: number_count } end private def match_count(word) text.scan(/#{word}/i).count end def capitalized_words_count words.count { |word| word.start_with?(word[0].upcase) } end def gender_dominance terms_count(%w(she her)) > terms_count(%w(he his)) ? 1 : 0 end def terms_count(terms) words.count { |word| terms.include?(word.downcase) } end def words_per_sentence_average sentences.count.zero? ? 0 : (words.count / sentences.count) end def quote_count text.scan(/"[^"]+"/).count end def number_count text.scan(/\d+[\.,]\d+|\d+/).count end end
Компиляция учебного набора данных
Мы хотим скомпилировать набор данных из наших текстовых функций и сохранить его в виде файла, чтобы мы могли позже загрузить его и обучить наш классификатор. Мы также могли бы делать все это в памяти, но при сохранении набора данных в виде файла мы можем изучить его и лучше понять, что на самом деле происходит на этом этапе. Weka предоставляет хороший способ сделать это с помощью класса Weka::Core::Instances
.
В отдельном скрипте загрузите наши учебные тексты и извлеките наши возможности. Создайте из них объект Instances
и, наконец, сохраните наш набор данных на нашем диске. Прежде чем мы начнем с этого, давайте создадим еще FileLoader
класс FileLoader
и Text
который будет красиво абстрагировать загрузку нашего файла и извлечение функций из данного файла.
FileLoader
вернет все текстовые файлы из данного каталога данных:
# file_loader.rb class FileLoader attr_reader :data_directory def initialize(data_directory) @data_directory = File.expand_path("../#{data_directory}", __FILE__) end def files_for(article_type) Dir.glob("#{data_directory}/#{article_type}/*.txt") end end
Наш класс Text
позволяет нам передавать текстовый файл и получать его функции, используя FeatureExtractor
мы создали выше:
# text.rb require_relative 'feature_extractor' class Text attr_reader :text def initialize(file) file_path = File.expand_path(file, __FILE__) # There seem to be some invalid UTF-8 characters in the texts, # so we remove them here. @text = File.read(file_path).encode!('UTF-8', 'UTF-8', invalid: :replace) end def features FeatureExtractor.new(text).features end end
Теперь мы можем использовать эти классы, чтобы написать скрипт для создания обучающего набора данных. Создайте новый файл с именем create_dataset.rb
. Сначала создайте пустой объект Instances
который представляет наш обучающий набор данных. Добавьте числовой атрибут для каждого объекта и номинальный атрибут класса. Мы настраиваем различные типы статей в качестве возможных значений класса:
# create_dataset.rb require 'weka' require_relative 'feature_extractor' require_relative 'file_loader' require_relative 'text' article_types = %i(athletics cricket football rugby tennis) attribute_names = FeatureExtractor.new('').features.keys dataset = Weka::Core::Instances.new.with_attributes do attribute_names.each do |name| numeric(name) end nominal(:class, values: article_types, class_attribute: true) end # ...
Затем вычислите функции для всех статей и добавьте их в наш объект instances
:
# ... def feature_list_for(article_type) files = FileLoader.new('data/training').files_for(article_type) files.map do |file| # Remember that Text#features returns a Hash. # We only need the feature values. # Since the class value is still missing, we append the # article_type as the class value. Text.new(file).features.values << article_type end end article_types.each do |article_type| feature_list = feature_list_for(article_type) dataset.add_instances(feature_list) end # ...
Наконец, мы можем сохранить все наши расчетные функции в файл в каталоге /generated
. Instances
позволяют сохранять и загружать наборы данных в различные форматы файлов, такие как CSV, JSON, ARFF и менее распространенный формат файлов C.45. Давайте выберем ARFF ( формат файла атрибута-отношения ), который был специально разработан для работы с наборами данных для задач машинного обучения и также хорошо читается людьми:
# ... dataset.to_arff('generated/articles.arff')
Запустите скрипт в своем терминале, чтобы создать обучающий набор данных:
$ jruby create_dataset.rb # If you're using RVM, this is just `ruby...`
Если вы быстро загляните в сгенерированный файл .arff
, вы увидите заголовок с настраиваемым именем отношения и определенными атрибутами, за которыми следуют фактические строки данных:
@relation Instances @attribute athletics_hints_count numeric @attribute cricket_hints_count numeric # ... @attribute class {athletics,cricket,football,rugby,tennis} @data 1,0,0,0,0,47,1,1237,11,3,19,2,1,0,7,athletics 1,0,0,0,0,46,1,901,7,2,20,0,0,2,5,athletics # ...
Скомпилировав наш учебный набор данных, мы можем продолжить обучение нашего классификатора и классифицировать наши тестовые статьи.
Тренировка классификатора
Есть множество различных встроенных классификаторов, из которых мы можем выбирать. Мы могли бы использовать классификаторы Байеса, нейронные сети, логистическую регрессию, деревья решений и многое другое. Для простоты мы будем использовать классификатор RandomForest . С RandomForest мы получаем простой в настройке классификатор, который основан на деревьях решений и хорошо справляется с общими проблемами.
Пришло время загрузить обучающий набор данных, а затем обучить классификатор RandomForest. Давайте сделаем это в новом файле с именем run_classification.rb
.
# run_classification.rb require 'weka' instances = Weka::Core::Instances.from_arff('generated/articles.arff') instances.class_attribute = :class classifier = Weka::Classifiers::Trees::RandomForest.new # The -I option determines the number of decision trees that are used in each # learning iteration, the default is 100, we increase it to 200 here to gain a # better performance. classifier.use_options('-I 200') classifier.train_with_instances(instances)
Обратите внимание, что мы должны вручную установить атрибут класса после того, как загрузили наш набор данных. Это необходимо, потому что в нашем файле ARFF нет информации о положении нашего атрибута класса (он не всегда должен быть последним!).
Это было достаточно просто. Наши тестовые статьи уже ждут нас!
Классификация тестовых статей
Теперь мы можем использовать наш обученный классификатор для классификации (давайте представим) немаркированных статей в нашем каталоге data/test
.
Прежде чем мы сможем передать наши тестовые статьи в классификатор, мы должны извлечь из них те же функции, что и для наших учебных текстов. К счастью, мы можем снова использовать наши FileLoader
и Text
:
# run_classification.rb require 'weka' require_relative 'file_loader' # <= added! require_relative 'text' # <= added! # ... article_types = %i(athletics cricket football rugby tennis) def feature_list_for(article_type) files = FileLoader.new('data/test').files_for(article_type) files.map do |file| # Remember again that Text#features returns a Hash. # We only need the feature values. # The class value is still missing, but this time, we append a "missing" # as class value. You can use nil, '?' or Float::NAN. Text.new(file).features.values << '?' end end article_types.each do |article_type| feature_list = feature_list_for(article_type) feature_list.map do |features| label = classifier.classify(features) puts "* article about #{article_type} classified as #{label}" end end
Здесь мы загружаем наши тестовые тексты и передаем их извлеченные функции в метод classify
нашего классификатора. После классификации распечатайте наши предсказанные классы на стандартный вывод.
Запустите скрипт и посмотрите на результат:
$ jruby run_classification.rb * article about athletics classified as athletics * article about athletics classified as athletics * article about cricket classified as cricket * article about cricket classified as cricket * article about football classified as football * article about football classified as football * article about rugby classified as rugby * article about rugby classified as rugby * article about tennis classified as tennis * article about tennis classified as tennis
Ура. Похоже, все наши статьи получили правильный ярлык!
Это не значит, что наша система классификации идеальна. При обучении классификаторов их эффективность может оцениваться с помощью подхода, называемого перекрестной проверкой # k-fold_cross-validation). Weka также дает нам метод cross_validate
для нашего классификатора.
Перекрестная проверка разделяет обучающий набор данных на N различных частей с равным количеством экземпляров. По умолчанию используется 10 сплитов. Затем для обучения классификатора требуется 9 подмножеств и классифицирует оставшийся набор. Это делается до тех пор, пока каждое подмножество не будет классифицировано после обучения классификатора другими 9 подмножествами. С помощью этой процедуры вы получите представление о том, насколько хорошо работает ваш классификатор, потому что вы уже знаете все метки и можете рассчитать определенные показатели.
Давайте посмотрим на 10-кратную перекрестную проверку для нашего классификатора:
evaluation = classifier.cross_validate(folds: 10) puts evaluation.summary # Correctly Classified Instances 602 82.8061 % # Incorrectly Classified Instances 125 17.1939 % # Kappa statistic 0.7708 # Mean absolute error 0.1223 # Root mean squared error 0.2281 # Relative absolute error 39.9808 % # Root relative squared error 58.3231 % # Coverage of cases (0.95 level) 97.9367 % # Mean rel. region size (0.95 level) 52.7373 % # Total Number of Instances 727
В первых двух строках мы видим, что наш классификатор правильно классифицировал только около 83% наших статей. Это на самом деле не так уж плохо для нашего маленького, надуманного набора функций. Вы можете ожидать улучшения производительности с набором тщательно отобранных функций. Теперь все зависит от вас — пусть начнется охота за лучшими функциями!
Вывод
В этой статье мы использовали JRuby для автоматической категоризации спортивных статей. Мы прошли три основных этапа построения системы классификации: извлечение элементов из необработанных текстов, создание набора обучающих данных и обучение классификатора. С нашим обученным классификатором мы классифицировали немаркированные статьи.
Похоже, что Ruby также может быть вашим лучшим другом для задач машинного обучения, и я очень рекомендую вам проверить среду Weka и немного поэкспериментировать с ней. Это не только хорошее упражнение, но и позволяет вам обнаружить, что базовое машинное обучение на самом деле не ракетостроение! Попробуйте и дайте мне знать, как это происходит.