Paradigmas de Programação
Published:
Agrupamento de Dados
A tarefa de agrupamento de dados consiste em particionar uma base de dados de tal forma que cada partição contém elementos que estão próximos entre si no espaço Euclidiano.
O problema das K-médias busca por k pontos, denominados centros, com o objetivo de minimizar a soma das distâncias entre cada ponto da base de dados e o centro mais próximo a ele.
A solução para esse problema é feita geralmente de forma heurística utilizando o algoritmo de Lloyd:
- Determine centros iniciais aleatórios
- Associe cada ponto da base com o centro mais próximo, gerando k partições
- Para cada partição, calcule um novo centro como a média dos pontos da partição
- Repita os dois passos anteriores por n repetições
Uma versão paralela desse algoritmo pode ser pensada da seguinte forma:
- Determine centros iniciais aleatórios
- Divida os dados em p pedaços, para cada pedaço faça em paralelo:
- Associe cada ponto do pedaço com o centro mais próximo, gerando k partições
- Para cada partição, calcule a soma dos pontos associados a ela e a quantidade de pontos
- Junte o resultado de cada pedaço, calculando a média
- Repita os dois passos anteriores por n repetições
Para essa tarefa utilizaremos o arquivo Wine_Quality_DataClean.csv como base de dados. Esse arquivo possui várias linhas e cada linha contém números do tipo Double. Cada linha do arquivo representa um ponto da base de dados. Nossa primeira tarefa é ler esse arquivo e gerar uma matriz do tipo Double, ou seja, um [[Double]].
Inicie um novo projeto com stack new kmeans simple
, no arquivo cabal acrescente na seção executable kmeans:
ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -eventlog -O2
other-modules: KMeansPar
build-depends: base >= 4.7 && < 5, parallel, formatting, clock, split, deepseq, array
No arquivo Main.hs substitua seu conteúdo por:
{-# LANGUAGE UnicodeSyntax #-}
{-# LANGUAGE OverloadedStrings #-}
module Main where
import System.Environment
import Formatting
import Formatting.Clock
import System.Clock
import Data.List.Split (chunksOf)
import Control.DeepSeq
main :: IO ()
main = do
[fname, sk, sit, schunks] <- getArgs
fileTrain <- readFile fname
let k = read sk :: Int
it = read sit :: Int
nchunks = read schunks :: Int
train = parseFile fileTrain
chunks = force $ chunksOf nchunks train
clusters = take k train
start <- getTime Monotonic
stop <- getTime Monotonic
fprint (timeSpecs % "\\n") start stop
return ()
Crie um arquivo KMeansPar.hs com o conteúdo:
{-# LANGUAGE BangPatterns #-}
module KMeansPar where
import Data.List
import Data.Function
import Control.Parallel.Strategies
import Data.Array
import Control.DeepSeq
Leitura do arquivo
A função readFile
tem a assinatura:
readFile :: FilePath -> IO String
Ela retorna o conteúdo de um arquivo em forma de String. Para transformar esse arquivo em uma lista de lista de Double, precisamos fazer:
- Separar cada
\\n
em um elemento da lista, gerando um[String]
- Dividir cada elemento dessa lista tokenizando a String pelo espaço, gerando um
[[String]]
- Converter cada elemento dessa lista de lista para o tipo
Double
As duas primeiras tarefas podem ser feitas utilizando as funções lines
e words
. Implemente a função parseFile
com a seguinte assinatura:
parseFile :: String -> [[Double]]
parseFile = ???
Algoritmo K-Means: funções e tipos base
No arquivo KMeansPar.hs vamos definir os tipos que trabalharemos, defina Ponto
e Cluster
como listas de Double
e defina um tipo ChunksOf a
como sendo uma lista de a
. Ele será utilizado para distribuir as tarefas entre os threads.
Vamos definir também alguns operadores auxiliares:
-- divide cada elemento de xs por x
(./) :: (Floating a, Functor f) => f a -> a -> f a
xs ./ x = (/x) <$> xs
-- eleva cada elemento de xs por x
(.^) :: (Floating a, Functor f) => f a -> Int -> f a
xs .^ x = (^x) <$> xs
-- soma dois vetores elemento-a-elemento
(.+.) :: (Num a) => [a] -> [a] -> [a]
(.+.) = zipWith (+)
-- subtrai dois vetores elemento-a-elemento
(.-.) :: (Num a) => [a] -> [a] -> [a]
(.-.) = zipWith (-)
length' :: Num b => [a] -> b
length' xs = fromIntegral $ length xs
Algoritmo K-Means: Associando pontos
Crie uma função assign
que recebe como parâmetro uma lista de centros, uma lista de pontos e retorna uma array contendo listas de pontos. A Array (da biblioteca Data.Array
) permite criar uma lista de chave valor cujos valores se acumulam. Exemplo:
accumArray (+) 0 (0,4) [(i `mod` 5,i) | i <- [1..10]]
Retornará uma lista de tuplas, com cada elemento contendo uma chave de 0 a 4 e o valor para uma chave k será a somatória de todos os i’s cujo resultado de i mod 5
retorne k. No nosso caso, queremos que a chave seja um inteiro de 0 a k-1 e o valor de uma chave i seja a lista de pontos cuja menor distância corresponde ao centro i. Ou seja:
assign :: [Cluster] -> [Ponto] -> Array Int [Ponto]
assign cs ps = accumArray (flip (:)) [zeroCluster] (0, k-1)
[(argmin (distancias p), p) | p <- ps]
where
distancias p = ??
euclid p c = ??
argmin xs = fst $ minimumBy (compare `on` snd) $ zip [0..] xs
zeroCluster = replicate (length $ head ps) 0
k = length cs
Escreva as funções distancias
e euclid
. A primeira deve mapear a aplicação da função euclid p
para cada centro e, a segunda, deve calcular a distância euclidiana entre um ponto p
e um centro c
.
Algoritmo K-Means: Somando os vetores de cada partição
Uma vez que agrupamos os pontos para cada partição, queremos somar esses vetores, resultando em um único vetor e armazenar quantos vetores foram alocados naquela partição. Escreva a função:
somaClusters :: Array Int [Ponto] -> [(Cluster, Double)]
Para recuperar uma lista de tuplas de uma array, aplique a função assoc
.
Algoritmo K-Means: um passo do algoritmo
Cada passo do algoritmo consiste da aplicação de somaClusters
ao resultado de assign cs ps
. Complete a função step
:
step :: [Cluster] -> [Ponto] -> [(Cluster, Double)]
step cs ps = force cs' -- força avaliação para não deixar o trabalho pra thread principal
where ???
Algoritmo K-Means: função principal
Finalmente a função principal kmeans
recebe como parâmetros a quantidade de iterações it
, pedaços de base de dados pss
, uma lista de clusters iniciais cs
e retorna uma lista de Clusters
. Utilizando Pattern Matching controlamos as repetições dos passos do algoritmo:
kmeans :: Int -> ChunksOf [Ponto] -> [Cluster]
-> [Cluster]
kmeans it pss cs
| it <= 0 = cs
| otherwise = kmeans (it-1) pss cs'
where
cs' = ???
O novo cluster cs'
deve ser construído pelos passos:
- Mapear a função
step
em cada pedaço de pontos em paralelo utilizandorseq
, resultando em uma lista de lista de tuplas - Somar o resultado dessa aplicação, somando os elementos das tuplas
- Filtrar o resultado eliminando as listas vazias
- Calcular a média em cada elemento da lista, resultando em uma lista de
Cluster
Finalizando
No arquivo Main.hs
acrescente a função principal:
start <- getTime Monotonic
let clusters' = kmeans it chunks clusters
print clusters'
stop <- getTime Monotonic
Compile o programa com stack build --profile
e execute com stack exec kmeans Wine_Quality_DataClean.csv 3 1000 1000 --RTS -- +RTS -N1
Execute e compara o algoritmo utilizando diferentes valores para o argumento -N.
Opcional: Intel Cloud
Compacte a pasta de seu projeto no formato tar.gz e copie para a sua área da Intel, substituindo host pelo hostname configurado:
$ scp kmeans.tar.gz host:~/
Faça login em sua conta via terminal e descompacte seu projeto com, faça o download do Stack, descompacte e instale:
$ ssh host
[host]$ tar zxvf kmeans.tar.gz
[host]$ wget https://get.haskellstack.org/stable/linux-x86_64.tar.gz
[host]$ tar zxvf linux-x86_64.tar.gz
[host]$ mv stack-1.7.1-linux-x86_64 stack
Edite o arquivo .bash_profile
e acrescente :$HOME/stack
ao final da linha do PATH
. Saia do ssh e entre novamente.
Na pasta kmeans crie um arquivo myjob com o conteúdo:
cd ~/kmeans
stack exec kmeans Wine_Quality_DataClean.csv 30 100 1000 --RTS -- +RTS -s -N2 -M2g -ls
Submeta um job com qsub myjob
, ao final da execução verifique a saída com:
[host]$ cat myjob.oID
onde ID
é o id do seu job. Verifique o tempo de execução para N = [1, 2, 4, 8, 16, 64].