Haskell TensorFlow. Решаем реальную задачу  

Библиотека TensorFlow предоставляет прикладной интерфейс как для Python, так и для Haskell. Работать на Python немного легче, но предметом нашей сегодняшней статьи будет Haskell. 

Далее мы займемся решением реальной задачи машинного обучения. Для этого нам понадобится набор данных «Ирисы» (Iris data set), содержащий параметры цветков трех различных видов ириса. Мы создадим полносвязную нейронную сеть (fully-connected neural network) и обучим ее определять вид растения по параметрам его цветка. 

Форматируем входные данные

Первым шагом при решении большинства задач машинного обучения является предобработка данных. Ведь не могут же данные волшебным образом преобразоваться в типы Haskell. К счастью, в этом нам может помочь отличная библиотека Cassava. Набор данных «Ирисы» представлен в виде CSV-файлов, каждый из которых содержит заголовок и серию наблюдений: 

120,4,setosa,versicolor,virginica 

6.4,2.8,5.6,2.2,2 

5.0,2.3,3.3,1.0,1 

4.9,2.5,4.5,1.7,2 

4.9,3.1,1.5,0.1,0 

 

Каждая строка представляет собой одно наблюдение и содержит 4 величины, характеризующие параметры цветка, а также метку класса. Набор данных охватывает растения трех видов: Iris Setosa, Iris Versicolor и Iris Virginica. Соответственно, последний столбец содержит три метки классов: 0, 1 и 2. 

Давайте создадим специальный тип, представляющий строку данных, который позволит нам обрабатывать файл построчно. Наш тип IrisRecord будет содержать признаки и метку класса. Этот тип сыграет роль моста между необработанными данными и тензорным форматом, с которым будет работать алгоритм машинного обучения. Для нашего типа мы используем класс типов Generic и создадим экземпляр FromRecord, что позволит нам с легкостью выполнить парсинг. Обратите внимание, мы не будем приводить секцию импорта во фрагментах кода. Полный список импорта представлен в конце статьи. Также отметим, что мы будем использовать расширение OverloadedLists. 

{-# LANGUAGE DeriveGeneric #-} 

{-# LANGUAGE OverloadedLists #-} 

... 

data IrisRecord = IrisRecord 

{ field1 :: Float 

, field2 :: Float 

, field3 :: Float 

, field4 :: Float 

, label  :: Int64 

} 
deriving (Generic) 

instance FromRecord IrisRecord 

Теперь у нас есть необходимый тип, и далее мы напишем функцию readIrisFromFile, которая будет читать данные из CSV-файла. 

readIrisFromFile :: FilePath -> IO (Vector IrisRecord) 

readIrisFromFile fp = do 

contents <- readFile fp 

let contentsAsBs = pack contents 

let results = decode  

HasHeader contentsAsBs :: Either String (Vector IrisRecord) 

case results of 

Left err -> error err 

Right records -> return records 

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

sampleSize :: Int 

sampleSize = 10 

chooseRandomRecords :: Vector IrisRecord -> IO (Vector IrisRecord) 

chooseRandomRecords records = do 

let numRecords = Data.Vector.length records 

chosenIndices <-  

take sampleSize <$> shuffleM [0..(numRecords - 1)] 

return $ fromList $ map (records !) chosenIndices 

Итак, мы отобрали данные и теперь необходимо преобразовать их в TensorData, чтобы передать алгоритму. Чтобы создать элементы TensorData, необходимо передать размерность и 1-мерный вектор значений. Для этого нам нужно знать размерности входа и выхода. И та и другая зависит от количества строк в выборке. Входные данные будут содержать 4 столбца – по одному на каждый признак. Выходные данные будут иметь 1 столбец, содержащий метку класса. 

irisFeatures :: Int64 

irisFeatures = 4 

irisLabels :: Int64 

irisLabels = 3 

convertRecordsToTensorData :: Vector IrisRecord  

-> (TensorData Float, TensorData Int64) 

convertRecordsToTensorData records = (input, output) 

where 

numRecords = Data.Vector.length records  

input = encodeTensorData  

[fromIntegral numRecords, irisFeatures] (undefined) 

output = encodeTensorData  

[fromIntegral numRecords] (undefined) 

Теперь осталось преобразовать записи в 1-мерные векторы для кодирования. Ниже представлена итоговая функция: 

convertRecordsToTensorData :: Vector IrisRecord  

-> (TensorData Float, TensorData Int64) 

convertRecordsToTensorData records = (input, output) 

where 

numRecords = Data.Vector.length records  

input = encodeTensorData  

[fromIntegral numRecords, irisFeatures] 

(fromList $ concatMap recordToInputs records) 

output = encodeTensorData  

[fromIntegral numRecords]  

(label <$> records) 

recordToInputs :: IrisRecord -> [Float] 

recordToInputs rec =  

[field1 rec, field2 rec, field3 rec, field4 rec] 

Основы нейронных сетей

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

data Model = Model 

{ train :: TensorData Float -- Training input 

-> TensorData Int64 -- Training output 

-> Session () 

, errorRate :: TensorData Float -- Test input 

-> TensorData Int64 -- Test output 

-> Session Float 

} 

Теперь приступим к разработке полносвязной нейронной сети. Сеть будет содержать 4 входных нейрона (по одному на каждый признак) и 3 выходных нейрона (по одному на каждый класс). Между входным и выходным слоем будет располагаться скрытый слой, содержащий 10 нейронов. Такая архитектура предполагает, что нам понадобится 2 массива весов (weight) и свободных членов (bias). Мы напишем функцию, которая будет принимать размерности и возвращать переменные тензоры для каждого слоя. Нам нужны тензоры весов и свободных членов, а также результирующий тензор слоя. 

buildNNLayer :: Int64 -> Int64 -> Tensor v Float 

-> Build (Variable Float, Variable Float, Tensor Build Float) 

buildNNLayer inputSize outputSize input = do 

weights <- truncatedNormal (vector [inputSize, outputSize]) >>= 

initializedVariable 

bias <- truncatedNormal (vector [outputSize]) >>= 

initializedVariable 

let results = (input `matMul` readValue weights) `add`  

readValue bias 

return (weights, bias, results) 

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

Создаем модель

Теперь приступим к созданию объекта модели Model опять же в пределах монады Build. Первым делом задаем плейсхолдеры для входа и выхода, а также количество скрытых нейронов. В качестве значения для batchSize указываем -1, поскольку мы хотим работать с пакетами различного размера. 

irisFeatures :: Int64 

irisFeatures = 4 

irisLabels :: Int64 

irisLabels = 3 

-- ^^ From above 

createModel :: Build Model 

createModel = do 

let batchSize = -1 -- Allows variable sized batches 

let numHiddenUnits = 10 

inputs <- placeholder [batchSize, irisFeatures] 

outputs <- placeholder [batchSize] 

Далее создаем переменные слоев и результатов. Между слоями добавляем функцию активации relu: 

(hiddenWeights, hiddenBiases, hiddenResults) <-  

buildNNLayer irisFeatures numHiddenUnits inputs 

let rectifiedHiddenResults = relu hiddenResults 

(finalWeights, finalBiases, finalResults) <- 

buildNNLayer numHiddenUnits irisLabels rectifiedHiddenResults 

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

actualOutput <- render $ cast $  

argMax finalResults (scalar (1 :: Int64)) 

let correctPredictions = equal actualOutput outputs 

errorRate_ <- render $ 1 - (reduceMean (cast correctPredictions)) 

Теперь рассмотрим процесс обучения. Сперва преобразуем метки классов в векторы, представленные в прямом унитарном коде oneHot. Это означает, что мы преобразуем метку 0 в вектор [1,0,0] и т.д. Мы будем сравнивать эти значения с нашими результатами (до выбора максимального значения) и таким образом реализуем функцию потерь (loss function). Затем сформируем список параметров, модифицируя которые, оптимизатор adam минимизирует нашу функцию потерь. 

let outputVectors = oneHot outputs (fromIntegral irisLabels) 1 0 

let loss = reduceMean $ fst $ softmaxCrossEntropyWithLogits finalResults outputVectors 

let params = [hiddenWeights, hiddenBiases, finalWeights, finalBiases] 

train_ <- minimizeWith adam loss params 

Мы подготовили errorRate_ и train_. Остался один последний шаг. Необходимо подключить плейсхолдеры и создать функции, которые будут принимать тензорные данные. Применим шаблон feed, рассмотренный здесь. Наконец, наша модель готова! 

return $ Model 

{ train = \inputFeed outputFeed ->  

runWithFeeds 

[ feed inputs inputFeed 

, feed outputs outputFeed 

] 

train_ 

, errorRate = \inputFeed outputFeed -> unScalar <$> 

runWithFeeds 

[ feed inputs inputFeed 

, feed outputs outputFeed 

] 

errorRate_ 

} 

Собираем все вместе

Теперь напишем главную функцию, которая будет запускать сессию. Она будет состоять из трех этапов. На подготовительном этапе загружаем данные и с помощью функции build создаем модель. Затем обучаем модель, выполняя 1000 итераций. Каждые 100 итераций выводим результат. Наконец, вычисляем итоговую ошибку модели с помощью тестовых данных. 

runIris :: FilePath -> FilePath -> IO () 

runIris trainingFile testingFile = runSession $ do 

-- Preparation 

trainingRecords <- liftIO $ readIrisFromFile trainingFile 

testRecords <- liftIO $ readIrisFromFile testingFile 

model <- build createModel 

   -- Training 

forM_ ([0..1000] :: [Int]) $ \i -> do 

trainingSample <- liftIO $ chooseRandomRecords trainingRecords 

let (trainingInputs, trainingOutputs) = 

convertRecordsToTensorData trainingSample 

(train model) trainingInputs trainingOutputs 

when (i `mod` 100 == 0) $ do 

err <- (errorRate model) trainingInputs trainingOutputs 

liftIO $ putStrLn $  

"Current training error " ++ show (err * 100) 

liftIO $ putStrLn "" 

   -- Testing 

let (testingInputs, testingOutputs) =  

convertRecordsToTensorData testRecords 

testingError <- (errorRate model) testingInputs testingOutputs 

liftIO $ putStrLn $ "test error " ++ show (testingError * 100) 

   return () 

 Результаты

Выполнив рассмотренный выше код, получим следующие результаты: 

Current training error 60.000004 

Current training error 30.000002 

Current training error 39.999996 

Current training error 19.999998 

Current training error 10.000002 

Current training error 10.000002 

Current training error 19.999998 

Current training error 19.999998 

Current training error 10.000002 

Current training error 10.000002 

Current training error 0.0 

 

test error 3.333336 

Тестовый набор данных содержал 30 примеров, и величина ошибки говорит о том, что мы правильно классифицировали 29 из 30. В различных прогонах результаты отличаются (я выбрал лучший из них). Поскольку размер выборки очень мал, наблюдается высокая энтропия (иногда ошибка может достигать 40%). Используя больший объем данных, можно добиться более стабильных результатов, но наш пример является хорошим началом. 

Заключение

В этой статье мы рассмотрели базовые аспекты создания нейронной сети с помощью Haskell TensorFlow. Мы разработали полносвязную нейронную сеть и обучили ее на реальных данных, загруженных с помощью библиотеки Cassava. Наша сеть обучилась классифицировать виды цветов из набора данных «Ирисы». Принимая во внимание малый объем данных, мы получили достаточно хорошие результаты. 

Подробную информацию по установке Haskell TensorFlow вы можете найти в руководстве, которое поможет вам запустить рассмотренный код на своем компьютере. 

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

Приложение. Весь импорт

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

import Control.Monad (forM_, when) 

import Control.Monad.IO.Class (liftIO) 

import Data.ByteString.Lazy.Char8 (pack) 

import Data.Csv (FromRecord, decode, HasHeader(..)) 

import Data.Int (Int64) 

import Data.Vector (Vector, length, fromList, (!)) 

import GHC.Generics (Generic) 

import System.Random.Shuffle (shuffleM) 

import TensorFlow.Core (TensorData, Session, Build, render, runWithFeeds, feed, unScalar, build, 

Tensor, encodeTensorData) 

import TensorFlow.Minimize (minimizeWith, adam) 

import TensorFlow.Ops (placeholder, truncatedNormal, add, matMul, relu, 

argMax, scalar, cast, oneHot, reduceMean, softmaxCrossEntropyWithLogits,  

equal, vector) 

import TensorFlow.Session (runSession) 

import TensorFlow.Variable (readValue, initializedVariable, Variable) 

Источник

1 комментарий

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

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

Ваш адрес email не будет опубликован.

закрыть

Поделиться

Отправить на почту
закрыть

Вход

закрыть

Регистрация

+ =