Машинное обучение с использованием Julia и ее экосистемы

Анализ набора данных Glass — часть 1

Julia работает быстро, может использоваться как интерпретируемый язык, имеет высокую степень компонуемости, но не является объектно-ориентированным. И у него есть быстрорастущая экосистема, которая помогает во всех аспектах типичного рабочего процесса ML.

Обзор учебных пособий

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

  • Часть I «Анализ набора данных Glass» посвящена предварительной обработке, анализу и визуализации данных с помощью таких пакетов, как ScientificTypes, DataFrames, StatsBase и StatsPlots.
  • Часть II «Использование дерева решений» посвящена сути рабочего процесса машинного обучения: как выбрать модель и как использовать ее для обучения, прогнозирования и оценки. Эта часть в основном зависит от пакета MLJ (= ​​Mмашина Lзаработок в Julia).
  • Часть III «Если что-то не «готово к использованию»» объясняет, как легко создать собственное решение с помощью нескольких строк кода, если доступные пакеты не предлагают всех необходимых вам функций. .

Введение

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

В этом уроке мы будем использовать набор данных стекло. Это набор данных для образовательных целей. Он поставляется с набором инструментов Weka и может быть загружен с разных веб-сайтов (например, здесь). Характеристики этого набора данных содержат информацию о различных вариантах стекла (например, о материалах, используемых для их производства, и оптических характеристиках), а целевой переменной является результирующий вариант стекла (см. отрывок ниже).

Данные поступают в формате файла под названием ARFF (формат файла атрибутов-отношений); формат, используемый набором инструментов Weka. Поэтому нам нужен пакет Julia с функциями для чтения этого формата: ARFFFiles.jl. Поскольку мы хотим преобразовать данные для дальнейшего использования в DataFrame, также необходим пакет DataFrames.jl. ScientificTypes.jl дополняет стандартные типы Julia функциями, специфичными для машинного обучения, и, что не менее важно, нам нужен пакет Downloads.jl для функциональности загрузки данных с веб-сайта.

Таким образом, код для использования этих пакетов, для загрузки данных с вышеупомянутого веб-сайта, для чтения их с диска и преобразования в DataFrame (и с использованием ScientificTypes) выглядит следующим образом:

Первый обзор набора данных

Чтобы получить первый обзор набора данных, мы извлекаем из него некоторую базовую информацию:

  • sz = size(glass) дает нам размеры: (214, 10), т.е. имеет 214 строк и 10 столбцов.
  • С помощью col_names = names(glass) мы получаем имена столбцов DataFrame:
10-element Vector{String}: 
“RI”, “Na”, “Mg”, “Al”, “Si”, “K”, “Ca”, “Ba”, “Fe”, “Type”
  • А schema(glass) (из ScientificTypes) дает нам типы данных всех столбцов. types — это типы данных Julia, тогда как scitypes — это уровень абстракции, представленный ScientificTypes. Здесь мы видим, что все признаки равны Continuous, а целевая переменная имеет номинальное значение (с семью разными записями).
schema(glass) -->
┌───────┬───────────────┬──────────────────────────────────┐
│ names │ scitypes      │ types                            │
├───────┼───────────────┼──────────────────────────────────┤
│ RI    │ Continuous    │ Float64                          │
│ Na    │ Continuous    │ Float64                          │
│ Mg    │ Continuous    │ Float64                          │
│ Al    │ Continuous    │ Float64                          │
│ Si    │ Continuous    │ Float64                          │
│ K     │ Continuous    │ Float64                          │
│ Ca    │ Continuous    │ Float64                          │
│ Ba    │ Continuous    │ Float64                          │
│ Fe    │ Continuous    │ Float64                          │
│ Type  │ Multiclass{7} │ CategoricalValue{String, UInt32} │
└───────┴───────────────┴──────────────────────────────────┘

Целевая переменная

Теперь давайте подробнее рассмотрим характеристики целевой переменной. glass_types = unique(glass.Type) дает нам список уникальных значений из столбца Type:

6-element Vector{String}:
 “build wind float”
 “vehic wind float”
 “tableware”
 “build wind non-float”
 “headlamps”
 “containers”

Таким образом, мы предполагаем, что в наборе данных есть 6 различных типов стекла. Но это только часть правды. Использование levels(glass.Type) из ScientificTypes показывает, что на самом деле существует 7 различных типов стекла:

7-element Vector{String}:
 “build wind float”
 “build wind non-float”
 “vehic wind float”
 “vehic wind non-float”
 “containers”
 “tableware”
 “headlamps”

Только 6 из них появляются в наборе данных о стекле (имея количество › 0). Тип 'vehic wind non-float' не встречается. Информация о его существовании хранилась в ARFF-файле и поддерживалась системой типов, установленной ScientificTypes. Это пример, в котором мы можем увидеть дополнительную ценность использования этой системы типов вместо того, чтобы просто полагаться на нативную систему типов языка (которая могла бы ввести нас в заблуждение в этом примере).

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

type_count = countmap(glass.Type) -->
Dict{CategoricalArrays.CategoricalValue{String, UInt32}, Int64} with 6 entries:
 “vehic wind float” => 17
 “tableware” => 9
 “build wind non-float” => 76
 “build wind float” => 70
 “headlamps” => 29
 “containers” => 13

Диаграмма, конечно, более информативна, чем просто список чисел. Итак, давайте визуализируем это с помощью гистограммы из пакета Plots:

bar(type_count, group = glass_types, legend = false,
 xlabel = “glass types”, xrotation = 20,
 ylabel = “count”)

Здесь мы также замечаем, что использовалась дополнительная информация из ScientifcTypes, таким образом показывая 'автомобильный ветер не плавает' с нулевым счетом.

Особенности в деталях

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

Приемлемы ли значения?

Давайте сначала проверим, являются ли значения этих функций разумными с технической точки зрения. Вызов describe из DataFrames дает нам, среди прочего, представление о диапазонах значений в наборе данных:

  • RI незначительно варьируется в районе 1,5. Из упомянутой выше страницы Википедии мы узнаем, что типичные значения RI для различных сортов стекла лежат в диапазоне 1,49 … 1,69. Таким образом, значения RI в наборе данных "glass" кажутся вполне разумными.
  • Другие функции - проценты. т.е. они должны быть в диапазоне от 0 до 100, что верно для наших данных.
  • Кремний, безусловно, является наиболее важным компонентом стекла (здесь › 70%), за ним следуют натрий и кальций.

Мы можем сделать еще одну проверку качества на проценты. В сумме они должны составлять до 100% для каждого состава стекла. Мы используем функцию transform из DataFrames и добавляем в каждую строку все значения признаков NaFe. Результаты сохраняются в новом столбце с именем sumpct:

glass_wsum = transform(glass, 
                  Between(:Na, :Fe) => ByRow(+) => :sumpct)

Взгляд на этот новый столбец и использование функций maximum и minimum из StatsBase показывает, что суммы для большинства строк не совсем равны 100 %, но они немного отличаются от этого значения. Это можно объяснить ошибками измерения и округления. Так что это нормально для нас.

maximum(glass_wsum.sumpct), minimum(glass_wsum.sumpct) --->
(100.09999999999998, 99.02)

Статистика и визуализации

Поскольку все атрибуты объектов относятся к типу Continuous, можно рассчитать некоторые статистические данные и построить диаграммы их распределения, чтобы получить больше информации.

Эксцесс и асимметрия

Чтобы получить первое представление о том, как выглядит распределение переменной с действительным значением, вычисляются две меры: эксцесс и асимметрия.

  • Асимметрия — это мера симметричности (точнее, асимметрии) распределения. Отрицательное значение указывает на то, что его левый хвост длиннее (и довольно плоский) и что масса сосредоточена справа. Мы говорим об перекосе влево или распределении с левым хвостом. С другой стороны, положительное значение указывает на перекос вправо распределения.
  • Эксцесс является мерой «хвостатости» распределения. Он указывает, насколько хвосты тянутся влево или вправо. Эксцесс нормального распределения равен 3. Поэтому эксцесс других распределений часто сравнивают с этим значением и вычисляют так называемый избыточный эксцесс. Он говорит, насколько эксцесс рассматриваемого распределения превышает эксцесс нормального распределения. Это также значение, которое мы получаем от функции kurtosis из пакета StatsBase.

Следующее выражение создает список этих мер для всех функций набора данных «стекло»:

[(atr, skewness(glass[:, atr]), kurtosis(glass[:, atr])) 
    for atr in col_names[1:end-1]]

Подобное выражение называется у Джулии «пониманием». Это массив, начальные значения которого вычисляются путем итерации. for atr in col_names[1:end-1] перебирает имена столбцов (от первого до второго, но последнего) glass. Для каждого имени столбца (atr) он создает кортеж с тремя элементами: само имя столбца atr, а также асимметрию (skewness(glass[:, atr])) и эксцесс (kurtosis(glass[:, atr])) этого столбца, таким образом создавая следующий вывод:

9-element Vector{Tuple{String, Float64, Float64}}:
 (“RI”, 1.6140150456617635, 4.7893542254570605)
 (“Na”, 0.4509917200116131, 2.953476583500219)
 (“Mg”, -1.1444648495986702, -0.42870155798810883)
 (“Al”, 0.9009178781425262, 1.9848317746238253)
 (“Si”, -0.7253172664513224, 2.8711045971453464)
 (“K”, 6.505635834012889, 53.392326556204665)
 (“Ca”, 2.0326773755262484, 6.498967959876067)
 (“Ba”, 3.392430889440864, 12.222071204852575)
 (“Fe”, 1.7420067617989456, 2.5723175586721645)

Из этих цифр мы уже можем видеть, что, например. распределения для K и Ba довольно асимметричны и имеют длинные хвосты. Но в большинстве случаев мы получаем еще больше информации, если визуализируем распределения.

Функция RI

Начнем с функции RI. Сюжет для скрипки, который можно создать с помощью функции violin из пакета StatsPlots, хорошо показывает, как выглядит его распределение. violin(glass.RI, legend = false) производит следующий график:

Гистограмма, столбцы которой окрашены в соответствии с целевым значением, дополнительно показывает взаимосвязь между значениями характеристик и результирующим типом стекла. Для этой цели мы можем использовать функцию histogram (тоже из StatsPlots; см. код Julia под диаграммой):

Функции NaFe

Мы можем сделать то же самое для остальных функций NaFe. Хорошей альтернативой скрипичному графику для изображения распределения является блочный график. Иногда сюжет для скрипки дает больше понимания, иногда — коробочный сюжет. Так что нам лучше создать оба. Если мы построим все три типа диаграмм для каждого объекта рядом, мы сможем легко сравнить их, в результате чего получится следующая сетка графиков:

Код Julia для создания этой сетки графиков выглядит следующим образом:

Первые три строки имеют схожую структуру: каждая строка создает массив диаграмм (сначала скрипки, затем диаграммы и, наконец, гистограммы). Опять же, мы используем «понимание» для этой цели: мы повторяем от 2 до 9, потому что нам нужна диаграмма для столбцов со 2 по 9 фрейма данных glass. Для каждого из этих столбцов мы создаем соответствующую диаграмму (так же, как мы делали выше для функции RI) с соответствующими параметрами (каждая диаграмма получает, например, имя функции в качестве своего xlabel).

Функция plot (которая строит всю сетку; строка 7) принимает в качестве аргумента массив подграфиков и упорядочивает их в сетке в соответствии с параметрами layout (здесь сетка 8 x 3). Таким образом, мы должны создать соответствующий массив, содержащий подграфики в правильном порядке (т. е. для каждого материала график скрипки, блок-диаграмму и гистограмму), применяя следующие шаги (код в строке 5):

  • hcat объединяет три массива violins, boxes и histos, создавая таким образом матрицу графиков 8 x 3 (по одному столбцу для каждого типа диаграммы).
  • Поскольку матрицы Джулии обрабатываются в порядке столбцов, мы меняем размеры с помощью permutedims, создавая матрицу 3 x 8 (поэтому три диаграммы для Na находятся в первом столбце, а диаграммы для Mg во втором столбце и так далее)
  • Наконец, мы должны «сгладить» матрицу в массив (что необходимо для plot), используя vec.

Корреляции

При работе с числовыми атрибутами всегда важно знать, коррелированы ли они и насколько сильно, поскольку некоторые модели плохо работают с сильно коррелированными атрибутами.

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

Матрица корреляции

Функция cor (из StatsPlots) дает нам нужную информацию в виде матрицы с корреляциями каждой пары признаков (так называемая матрица корреляции):

Как всегда, визуализация делает цифры более понятными. В этом случае подходящим типом диаграммы является тепловая карта (которую мы создаем с помощью heatmap from StatsPlots). Поскольку корреляционная матрица симметрична, достаточно визуализировать ее нижнюю часть (которую можно извлечь с помощью LowerTriangular из пакета LinearAlgebra; см. код Julia под диаграммой). Цветовая шкала :tol_sunset, используемая для тепловой карты, взята из пакета ColorSchemes.

heatmap(LowerTriangular(cor_mat), yflip = true,
 seriescolor = :tol_sunset,
 xticks = (1:9, col_names[1:9]),
 yticks = (1:9, col_names[1:9]))

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

  • Ca/RI → 0.810403
  • Ba/Al → 0.479404
  • Ba/Na → 0.326603
  • K/Al → 0.325958

… при этом только первые два являются действительно «сильными», если мы посмотрим на цифры.

Заметные отрицательные корреляции можно наблюдать с:

  • Si/RI = -0.542052
  • Ba/Mg = -0.492262
  • Al/Mg = -0.481798
  • Ca/Mg = -0.443750

График корреляции и угловой график

Следующие графики дают еще более подробное представление о корреляциях между парами различных атрибутов:

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

Каждый из этих графиков был создан всего одной строкой кода Julia с использованием следующих функций из StatsPlots:

@df glass corrplot(cols(1:9))
@df glass cornerplot(cols(1:9), compact = true)

Выводы

Приведенные выше примеры показали, как можно выполнить важные шаги типичного рабочего процесса машинного обучения, используя всего несколько строк кода. Часть II руководства продолжает это путешествие, уделяя особое внимание таким этапам, как обучение, прогнозирование и оценка.

Также стало понятно, как легко можно комбинировать функционал из разных пакетов Julia и насколько хорошо они работают вместе, даже если разрабатывались независимо друг от друга. Часть III учебника объяснит внутреннюю работу такой компонуемости. Так что следите за обновлениями!

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

Дальнейшая информация