Хабрахабр

Julia и нейронные сети: Flux

С тех пор язык набирает популярность: он используется в более чем 1500 университетах (в некоторых преподается в качестве первого ЯП), а области применения охватывают от медицинской диагностики и планирования космических миссий до таких насущных проблем, как оптимизация трафика школьных автобусов.
Прошло чуть больше года, с тех пор как MIT объявил о релизе высокопроизводительного языка общего назначения Julia.

Одним из ключевым полей деятельности многих проектов, как не трудно догадаться, является машинное обучение, для которого на Julia есть множество мощных инструментов, а недавно вышел в свет довольно интересный проект — Система вероятностного программирования общего назначения "GEN".

Постараемся пройти путь от обработки и исследования наборов изображений до обученной нейронной сети, чтобы получить полноценный классификатор! Сегодня же мы обратим внимание на, как понятно из названия, пакет Flux, предоставляющий всю мощь нейронных сетей.

Установка

С официального сайта скачиваем дистрибутив и устанавливаем интерпретатор Julia (REPL) на свой компьютер.

Для корректной работы менеджера пакетов пользователи Windows 7 / Windows Server 2012 также должны установить:

Процесс работы в REPL выглядит как-то так:

Здесь можно посмотреть про установку, а также найти интерактивные уроки для самостоятельного изучения с заданиями на русском (ссылки на оригинальные туториалы и руководство по языку там же). Настоящие же датасаянтисты и машинлёнингологи предпочитают Jupyter.

Здесь можно посмотреть на то, как работать с Jupyter Notebook.

Если проблемы при установке

  • Не удается установить соединение — проверьте свои права доступа (нет ли у вас ограничений на запись в папки на C:\, зайдите как админ или запустите Джулию в режиме администратора), если используете прокси, убедитесь, что оно настроено не только для браузера
  • Некоторые пакеты не любят кириллицу в пути файлов, так что из-за имени пользователя на русском у меня было много проблем
  • Для корректной работы некоторых пакетов на Windows нужно чтобы пути к Julia и Jupyter были занесены в переменные среды.

2. Компьютер/Свойства сиситемы/Дополнительные параметры системы/Переменные среды/Path (Создать если нет) и добавить туда путь к julia.exe
Пример C:\Users\User\AppData\Local\Julia-1. 0\bin
если в Path уже есть значения, то отделяем их точкой с запятой.
Теперь если в командную консоль (cmd) вбить julia то запустится интерпретатор.

Вводите команды в REPL или Jupyter Установив все необходимое, можете переходить к загрузке пакетов необходимых на сегодня.

Код

using Pkg
pkgs = ["Plots", "TextParse", "CSV", "DataFrames", "ImageMagick", "Images", "Interact", "Flux"] for p in pkgs Pkg.add(p)
end for p in pkgs Pkg.build(p)
end

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

Загрузка и обработка данных

Касательно Julia в сети много устаревшего материала, но для начала можно попробовать указанный выше самоучитель, а для более тщательного изучения ознакомьтесь с книгой Data Science with Julia (в свободном доступе) Сбор данных и их упорядочивание это отдельное искусство.

А сегодня, пожалуй, поработаем с уже подготовленными данными: датасетом из огромного количества фотографий фруктов с различных ракурсов — кто хотел фруктового фреша?

Собственно это и есть задание — будем учить нейронную сеть отличать яблоки от бананов!

Перво-наперво подгрузим несколько тестовых изображений:

using Images fnames = [ "data/10_100.jpg", "data/107_100.jpg", "data/yellow_apple_2.jpg", "data/8_100.jpg", "data/104_100.jpg", "data/3_100.jpg"
]
# массив картинок
fruits = [load(fname) for fname in fnames] hcat(fruits...) # соединить в одно изображение

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

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

using Statistics: mean M1 = [ mean(float.(c.(img))) for c = [red,green,blue], img = fruits ] 3×6 Array: 0.570278 0.652852 0.977111 0.835252 0.903998 0.842564 0.338118 0.468729 0.950773 0.806882 0.880692 0.755442 0.322406 0.379424 0.835212 0.707626 0.799643 0.761916

Желтое яблоко и бананы краснее чем яблоки сорта "Бребурн"! Внимательно смотрим на первую строчку — ничего не смущает? Да ладно вам корчить кислые мины, может этот туториал школьники читают, или младшекурсницы из балетно-тракторного института. Как так?! Дело в том, что фон каждой картинки имеет белый цвет, а он в нотации RGB представлен значениями (1,1,1). Поэтому постараемся избегать недомолвок. Для наглядности, расщепим изображения по базовым цветам: А так как на изображениях 3 тире 6 фона больше, плюс раскраска бананов и желтого яблока тоже содержит красный цвет, вот и выходит, что первые две картинки проигрывают в красноте.

function tweaking(img) R = colorview( RGB, red.(img),zeroarray,zeroarray ) G = colorview( RGB, zeroarray,green.(img),zeroarray ) B = colorview( RGB, zeroarray,zeroarray, blue.(img) ) [R; G; B]
end tweaking( hcat(fruits...) )

Чем чернее — тем меньше определенного цвета, и как мы и предполагали, фон своей насыщенностью зашумляет нам вычисление средних. Слышали когда-нибудь загадочное слово "базис?" Так вот можно сказать что эти изображения разложены в RGB-базисе. Удалим его.

function remove_background(img) mtrx = copy( channelview(img) ) for i = 1:size(mtrx, 2), j = 1:size(mtrx, 3) if reduce(&, mtrx[:,i,j] .> [0.8, 0.8, 0.8]) # а точнее - усредним mtrx[:,i,j] .= [0.5, 0.5, 0.5] end end colorview(RGB, mtrx)
end greyfruits = remove_background.(fruits)

M3 = [ mean(float.(c.(img))) for c = [red,green,blue], img = greyfruits ] 3×6 Array{Float32,2}: 0.451008 0.532696 0.578967 0.527727 0.52849 0.500276 0.218805 0.348609 0.552679 0.499192 0.505136 0.412946 0.203528 0.260142 0.439354 0.400631 0.424784 0.419291

Это и будет критерием оценки, то бишь — признаком. Все еще сказывается разница в площадях, которую занимает каждый объект, но в целом, можно сделать вывод, что бананы зеленее (и синее) яблок. Теперь пошерудим по остальным картинкам:

pth = "C:\\Users\\User\\Desktop\\Banana" # Apple Braeburn
fnames = readdir(pth)[1:300] 300-element Array{String,1}: "0_100.jpg" "104_100.jpg" "107_100.jpg" "10_100.jpg" "112_100.jpg" "117_100.jpg" "118_100.jpg" "119_100.jpg" ...

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

dataz = []
for fname in fnames img_i = load("$pth\\$fname") gbimg = remove_background(img_i) colorz = [ mean(c.(gbimg)) for c = [red,green,blue] ] inform = [size(gbimg, 1) size(gbimg, 2) colorz' ] push!(dataz, inform)
end
dataz

… и далее можно оформить наши данные в удобные для работы структуры — датафрэймы:

using DataFrames, CSV banans = DataFrame( vcat(dataz...), [:height, :width, :red, :green, :blue] )
CSV.write("data/bananas.csv", banans) # запись в файл

apples = CSV.read("data/Apple_Braeburn.csv") # считывание из файла
banans = CSV.read("data/bananas.csv")

Desc = describe(apples, :all) # боги смерти едят только яблоки

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

function plot2features(clr) x_apples = apples[:, :green] x_banans = banans[:, :green] y_apples = apples[:, clr] y_banans = banans[:, clr] scatter(x_apples, y_apples, lab = "apples", colour = :red) scatter!(x_banans, y_banans, lab = "bananas", legend = :topleft, colour = :yellow) hline!([mean(y_apples), mean(y_banans) ], lab = "" ) vline!([mean(x_apples), mean(x_banans) ], lab = "" ) xaxis!("green") yaxis!("$clr")
end plot2features(:red)

plot2features(:blue)

А вот на втором графике уже более явно прослеживается обособленность фруктов сразу по двум цветовым признакам. Средне-банановое красное очень близко по значению средне-яблочному. 2 до 0. Обособленности можно улучшить правильной перенормировкой, например наши значения зеленого меняются от 0. 55, а если выполнить преобразование

$ x_i' = \frac{x_i - \min(x)}{\max(x) - \min(x)} $

то мы получим данные перемасштабированные на [0,1], что увеличит зазор между этими кучками скоплениями точек.

Перцептрон

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

Впоследствии Фрэнк Розенблатт предложил обучаемую нейронную сеть — перцептрон. Наиболее известная и популярная для этих целей модель – это искусственный нейрон Маккалока — Питтса, разработанный в начале 1940-х годов. Про нейронные сети нетрудно найти исчерпывающие разъяснения, в том числе на данном ресурсе (Например Нейронные сети для начинающих, Применение нейросетей в распознавании изображений, Нейронные сети, фундаментальные принципы работы, многообразие и топология )

Выбрав в качестве функции активации сигмоиду и задав в соответствие ее выходам обозначения классифицуруемых объектов (фруктов)

$ \sigma(x; w, b) := \frac{1}{1 + \exp(-wx + b)}$

$ x = \mathrm{data} $

$ \sigma(x;w,b) \approx 0 \implies \mathrm{apple} $

$ \sigma(x;w,b) \approx 1 \implies \mathrm{banana}$

подберем такие параметры $W$ и $b$, чтобы выходные значения сигмоиды для получаемых данных соответствовали принятым выше обозначениям

using Interact sigmo(x,w,b) = 1 / (1 + exp(-w*x+b)) r_apples, g_apples, b_apples = apples[:, :red], apples[:, :green], apples[:, :blue]
r_banans, g_banans, b_banans = banans[:, :red], banans[:, :green], banans[:, :blue]; @manipulate for w in 10:1:60, b in -5:1:25 plot(x->sigmo(x,w,b), 0, 1, label="Model", legend = :topleft, lw=3) scatter!(g_apples[1:5], zeros(10), label="Apple", colour = :red) scatter!(g_banans[1:5], ones(10), label="Banana", colour = :yellow) end

foon(x) = sigmo(x,60,24)
plot(foon, 0, 1, label="Model", legend = :topleft, lw=3)
scatter!(foon, g_apples, label="Apple", colour = :red)
scatter!(foon, g_banans, label="Banana", colour = :yellow)
xaxis!("green")

Мы вручную научили нейрончик отличать яблоки от бананов по количеству зеленого цвета!

Введем функцию потерь Совершенно естественно желание автоматизировать этот процесс.

$ L(w,b) = (0 - σ(x_1, w, b))^2 + (1 - σ(x_2, w, b))^2 $

Теперь процесс обучения будет состоять в минимизации этой функции:

Код

apples_mean_green = mean(g_apples)
banans_mean_green = mean(g_banans) L(w, b) = (0 - sigmo(apples_mean_green,w,b))^2 + (1 - sigmo(banans_mean_green,w,b))^2 w_range = 10:0.5:30
b_range = 0:0.5:20 L_values = [L(w,b) for b in b_range, w in w_range] @manipulate for w in w_range, b in b_range p1 = surface(w_range, b_range, L_values, xlabel="b", ylabel="w", cam=(80,40), cbar=false, leg=false) scatter!(p1, [w], [b], [L(w,b)+1e-2], markersize=5, color = :blue) p2 = plot(x->sigmo(x,w,b), 0, 1, label="Model", legend = :topleft, lw=3) scatter!(p2, [apples_mean_green], [0.0], label="Apple", markersize=10) scatter!(p2, [banans_mean_green], [1.0], label="Banana", markersize=10, xlim=(0,1), ylim=(0,1)) plot(p1, p2, layout=(2,1))
end

К счастью самые необходимые уже имеются в окружении Flux! Ранее мы изучали пакеты для Julia позволяющие решать задачи оптимизации различными методами.

Flux

using Flux

Для начала представим данные для обучения в удобоваримом виде:

Y = [zeros(length(g_apples)); ones(length(g_banans)) ] |> permutedims
X = [g_apples; g_banans] |> permutedims;
# один признак - количество зеленого
# dataz = repeated((X, Y), 20)

Далее по порядку:

  • Создаем датасет для обучения, объединив входные данные с правильными ответами касательно классификации этих данных
  • Задаем параметры W и b матрицами случайных значений (на входе один признак и на выходе один, поэтому матрицы размера 1 x 1)
  • В качестве модели задаем плотный слой — перцептрон с сигмоидальной функцией активации
  • Задаем функцию потерь — сумма квадратов разностей (еще можно использовать более популярную Flux.crossentropy())
  • В качестве метода оптимизации выбираем градиентный спуск. Она принимает параметр — скорость спуска
  • Задаем функцию оценки, которая будет округлять значения выходов модели и сравнивать их с правильными ответами.
  • И распечатаем параметры нашей необученной модели

dataz = [(X, Y)]
W = param(rand(1))
b = param(rand(1))
model = Dense(W, b, σ)
loss(x, y) = mse(model(x), y)
opt = Descent(0.1)
accuracy(x, y) = mean( round.(model(x)) .== y )
params(model) Params([[0.3372841444115968] (tracked), [0.8430399003786011] (tracked)])

Посмотрим чему равен выход функции потерь для наших данных

loss(X, Y)
# чем меньше, тем лучше
0.310845210182773 (tracked)

И проверим результаты функции оценки

accuracy(X, Y) 0.5

Результат вполне закономерен — выходы распределены довольно однородно и правильно классифицируется половина данных:

Код

modeldataz(x) = x |> model |> data |> permutedims
# modeldataz(x) = permutedims(data(model(x)))

modelX = modeldataz(X)
modelapples = modeldataz(g_apples')
modelbanans = modeldataz(g_banans') plot(modelX, legend = false)
hline!([0.5])
p1 = yaxis!((0,1)) curv = [-1:0.01:1;]' |> modeldataz
plot( [-1:0.01:1;], curv, label="Model", legend = :topleft, lw=3) scatter!(g_apples, modelapples, label="Apple", colour = :red)
scatter!(g_banans, modelbanans, label="Banana",colour = :yellow)
hline!([0.5], lab = "", legend = :topleft)
p2 = xaxis!("green") plot(p1, p2)

Надо только прикрикнуть на нейросеть: "Тренируйся!", указав при этом, на чем тренироваться и что минимизировать, и она выполнит один проход обучения. Приступим к обучению: это довольно просто. Посему, заставим ее отучить всё как следует, но только без фанатизма, чтоб не вышло переобучения

for i in 1:7000 train!(loss, params(model), dataz, opt)
end model.W, model.b ([9.578663260720564] (tracked), [-3.7540362587506464] (tracked))

Потери стали гораздо меньше:

loss(X, Y) 0.09152783090457564 (tracked)

А оценка — лучше:

accuracy(X, Y) 1.0

Проверим обученную модель на самом первом наборе фруктов: Данные разделились, а дальнейшее обучение будет делать модельную функцию более вертикальной.

function classifier(img) gbimg = remove_background(img) greenmean = [ mean(float.(c.(gbimg))) for c = [red,green,blue] ] answ = data( model( [ greenmean[2] ]' ) )[1] fr = answ > 0.5 ? "Banana" : "Apple" "$fr $(round(200abs(0.5-answ)))%"
end hcat(fruits...)

classifier.(fruits) 6-element Array{String,1}: "Apple 68.0%" "Apple 20.0%" "Banana 65.0%" "Banana 47.0%" "Banana 49.0%" "Banana 10.0%"

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

Или можно использовать не RGB представление, а HSV (hue, saturation, value), в котором канал hue будет содержать информацию о цвете изображения.

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

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

MNIST

using Images
using Flux, Flux.Data.MNIST, Statistics
using Flux: onehotbatch, onecold, crossentropy, throttle
using Base.Iterators: repeated
# using CuArrays # Classify MNIST digits with a simple multi-layer-perceptron imgs = MNIST.images()
# Stack images into one large batch
X = hcat(float.(reshape.(imgs, :))...);
hcat(imgs[1:10]...)

Здесь пригодятся так называемые One-hot вектора Пример интересен тем, что выходов аж десять.

labels = MNIST.labels()
# One-hot-encode the labels
Y = onehotbatch(labels, 0:9)

10×60000 Flux.OneHotMatrix{Array{Flux.OneHotVector,1}}: 0 1 0 0 0 0 0 0 0 0 0 0 0 … 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 … 0 0 0 0 0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0

В качестве модели зададим цепочку нейронов, функцией потерь будет перекрестная энтропия, а в качестве метода оптимизации — Adam:

m = Chain( Dense(28^2, 32, relu), Dense(32, 10), softmax) loss(x, y) = crossentropy(m(x), y) accuracy(x, y) = mean(onecold(m(x)) .== onecold(y)) dataset = repeated((X, Y), 20)
evalcb = () -> @show(loss(X, Y))
opt = ADAM()

Тренируется пускай в щадящем режиме, но распечатывая потери каждые 10 секунд:

for i = 1:10 Flux.train!(loss, params(m), dataset, opt, cb = throttle(evalcb, 10))
end
# можно пока поесть...

accuracy(X, Y) 0.64545

И проверим на данных не использованных при обучении

# Test set accuracy
tX = hcat(float.(reshape.(MNIST.images(:test), :))...)
tY = onehotbatch(MNIST.labels(:test), 0:9) accuracy(tX, tY) 0.6488

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

Всем умеренного процессорного тепла!

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть