Clustering
O clustering ou análise de agrupamento de dados é o conjunto de técnicas de prospecção de dados (data mining) que visa fazer agrupamentos automáticos de dados segundo o seu grau de semelhança. O critério de semelhança faz parte da definição do problema e, dependendo, do algoritmo. A cada conjunto de dados resultante do processo dá-se o nome de grupo, aglomerado ou agrupamento (cluster).
O procedimento de agrupamento (clustering) também pode ser aplicado a bases de texto utilizando algoritmos de prospeção de texto (text mining), onde o algoritmo procura agrupar textos que falem sobre o mesmo assunto e separar textos de conteúdo diferentes.
Normalmente o usuário do sistema deve escolher a priori o número de grupos a serem detectados. Alguns algoritmos mais sofisticados pedem apenas o número mínimo, outros tem a capacidade de subdividir um grupo em dois.
Os tipos de algoritmos de agrupamento de dados mais comuns são: os particionais e os hierárquicos.
Função distância
editarQuando se tem um conjunto de objetos é natural olhar para eles tentando perceber o quão semelhantes ou diferentes eles são uns dos outros. Uma abordagem comum consiste em definir uma função de distância entre os objetos, com a interpretação de que objetos a uma distância menor são mais semelhantes uns aos outros. Assim, a distância assume um significado mais abstrato. Por exemplo, podemos definir a distância entre pessoas como a diferença do ano em que nasceram. O clustering, que traduz-se em agrupamento, surge quando tentamos classificar ou organizar objetos em grupos coerentes. Dada uma função de distância, o problema pretende dividi-los em grupos para que, intuitivamente, objetos dentro do mesmo grupo estejam "próximos", e em diferentes grupos, "distantes".
Agrupamento de máximo espaçamento
editarSuponha que temos um conjunto de n objetos . Para cada par, e , temos uma distância d numérica . Exigimos apenas que para todo =j; que para e distintos; e que as distâncias sejam simétricas, ou seja, que . Suponha que queremos dividir os objetos de em k grupos, para um determinado parâmetro k. Denominamos por k-grupo de cada elemento duma qualquer partição de em k conjuntos não vazios . Definimos ainda o espaçamento de um k-grupo, como sendo a distância mínima entre qualquer ponto pertencente a esse k-grupo e qualquer ponto situado num k-grupo diferente. Sendo o objetivo do agrupamento de dados a criação de grupos bem distintos entre si, e portanto com elevadas distâncias entre pontos pertencentes a grupos diferentes, um objetivo natural é buscar k-grupos com o máximo espaçamento possível. Entretanto, há muitos k-grupos diferentes num conjunto . O problema agora se resume em encontrar de forma eficiente a partição de que nos conduza ao espaçamento máximo.
Algoritmo de agrupamento
editarPara a argumentação do funcionamento do algoritmo, necessita-se do conceito de árvore de extensão mínima, também conhecida por minimum spanning tree.
Árvore de extensão mínima
editarO problema da árvore de extensão mínima se resume, basicamente, em buscar a forma mais barata de conectar todos os nós de um grafo, uma vez que podemos usar apenas um subconjunto de suas arestas para conectá-los. Isto pode ser útil na construção de uma rede elétrica, na construção do núcleo de uma rede rodoviária ou ferroviária, no estabelecimento de um circuito, ou mesmo na realização de algumas formas de agrupamento.[1]
Existem diversos algoritmos para encontrar uma árvore de extensão mínima. Aqui, vamos mostrar um algoritmo guloso, o algoritmo de Prim.
Algoritmo de Prim
editarTomamos um nó no grafo como inicial e todas as arestas que o tem como extremidade como arestas viáveis. Adicionamos ao novo grafo a aresta viável de menor custo e, consequentemente, o nó recém descoberto de sua outra extremidade. Assim, o conjunto de arestas viáveis passa ser todas as arestas que tem como extremidade ao menos um dos nós já descobertos. Seguimos adicionando as arestas de menor custo desde que um dos nós de suas extremidades ainda não esteja descoberto. Não entraremos no mérito de demonstrar que o algoritmo de Prim de fato funciona e é eficiente, entretanto daremos a intuição. A cada passo do algoritmo, encontramos uma árvore de extensão mínima para um subgrafo contido no grafo original. Digamos que o algoritmo é executado a partir de um nó inicial v, construindo assim um novo grafo. Sendo (v,u) a aresta de menor custo que liga v a um de seus vizinhos, digamos u, adicionaremos ela ao novo grafo e, consequentemente, teremos uma árvore de extensão mínima para o subgrafo formado pelos nós u e v. Repetindo esse processo, teremos uma árvore de extensão mínima para um dado grafo ao termos adicionado todos os seus nós ao novo grafo.
Descrição do algoritmo
editarAgora temos o conhecimento necessário para compreender o algoritmo de encontrar k grupos de máximo espaçamento de um determinado grafo conexo.
Ao deletarmos uma aresta de uma árvore, criamos dois componentes conexos que também são árvores, tal fato é usado fortemente neste algoritmo. Encontrar k grupos consiste em deletar arestas de um grafo até obtermos k componentes conexos. No problema que estamos tratando, desejamos, além de encontrar k componentes conexos, que estes componentes sejam o mais distantes possível entre si. Definiremos o espaçamento de um conjunto de grupos de um grafo como a menor distância entre dois nós que pertencem a componentes conexos distintos. Provaremos agora, que ao deletar as k-1 maiores arestas de uma árvore de extensão mínima de um grafo conexo U, obteremos k grupos e o espaçamento entre eles será máximo.[2]
A otimalidade do algoritmo
editarTome o conjunto de grupos (ou clusters) que particiona um grafo conexo . O espaçamento de é o comprimento da (k-1)-ésima aresta mais custosa da árvore de extensão mínima, que é a última aresta deletada pelo algoritmo de agrupamento. Para confirmarmos essa afirmação, precisamos provar que o espaçamento de qualquer outro conjunto de k grupos é, no máximo, d*. Seja um outro conjunto de k grupos que também particiona o grafo em conjuntos não vazios .Como e são diferentes, existe, necessariamente, um grupo que não é subconjunto de nenhum pertencente a . Logo, existem pontos e em que pertencem a grupos diferentes em - digamos pertencente a e pertencente a diferente de . Dado que e pertencem a um mesmo grupo, nenhuma das arestas do caminho entre eles foi deletado pelo algoritmo. Ou seja, cada aresta pertencente a tem comprimento menor ou igual a . Por outro lado, sabemos que pertence a , mas não; logo, tomemos como o primeiro nó em que não pertence a e o nó que vem logo antes de no caminho . Sabemos que em e isso completa a prova, pois o , sendo o espaçamento de .
Implementação e análise da complexidade
editarImplementar este algoritmo consiste em descobrir uma maneira eficiente de encontrar a aresta menos custosa dentro de um subconjunto de arestas do grafo original - para construir a árvore de extensão mínima - e a mais custosa - para deletá-la na segunda etapa do algoritmo. Tais operações são facilmente realizadas com o uso de uma fila de prioridades que pode ser mantida com complexidade , onde m é o número de elementos na fila. A complexidade de montar a árvore de extensão mínima é , onde é o número de nós no grafo e o número de arestas. Já a complexidade de deletar k-1 arestas é . Logo, o tempo de execução é dominado pela construção da árvore de extensão mínima. Conclui-se, então, que encontrar k grupos em um grafo com nós e arestas é uma tarefa que possuí tempo de execução .
Código em Racket
editar(require data/heap)
(define (neib g node)
(dict-ref g node))
(define (nodes g)
(map (lambda (x) (car x)) g))
(struct edge (name val))
(define (edge<-list lst)
(edge (cdr lst) (car lst)))
(define (list<-edge edg)
(cons (edge-val edg) (edge-name edg)))
(define (edge<=? x y)
(<= (edge-val x) (edge-val y)))
(define (edge>=? x y)
(>= (edge-val x) (edge-val y)))
(define (minimum-spanning-tree g x)
(let ([v-checked (make-vector (length g) 0)]
[res empty]
[binary-tree (make-heap edge<=?)])
(vector-set! v-checked (- x 1) 1)
(heap-add-all! binary-tree (map (lambda (y) (edge<-list y)) (neib g x)))
(let loop ()
(let ([u (heap-min binary-tree)])
(heap-remove-min! binary-tree)
(when (or (equal? (vector-ref v-checked (- (car (edge-name u)) 1)) 0)
(equal? (vector-ref v-checked (- (cdr (edge-name u)) 1)) 0))
(set! res (cons (list<-edge u) res))
(if (equal? (vector-ref v-checked (- (car (edge-name u)) 1)) 0)
(and (vector-set! v-checked (- (car (edge-name u)) 1) 1)
(heap-add-all! binary-tree (map (lambda (y) (edge<-list y)) (neib g (car (edge-name u))))))
(and (vector-set! v-checked (- (cdr (edge-name u)) 1) 1)
(heap-add-all! binary-tree (map (lambda (y) (edge<-list y)) (neib g (cdr (edge-name u)))))))))
(when (not (equal? (heap-count binary-tree) 0))
(loop)))
(reverse res)))
(define (k-cluster g k)
(let ([binary-tree (make-heap edge>=?)]
[count (- k 1)]
[mstree (minimum-spanning-tree g 1)])
(heap-add-all! binary-tree (map edge<-list mstree))
(let loop()
(heap-remove-min! binary-tree)
(set! count (- count 1))
(when (not (equal? count 0))
(loop)))
(map list<-edge (vector->list (heap->vector binary-tree)))))
;Exemplo de grafo
(define grafo '(1 . ((7 . (1 . 2)) (1 . (1 . 4)) (2 . (1 . 5)) (2 . (1 . 7)) (5 . (1 . 8))))
(2 . ((7 . (1 . 2)) (1 . (2 . 6)) (6 . (2 . 8)) (5 . (2 . 9))))
(3 . ((1 . (3 . 4)) (2 . (3 . 5)) (2 . (3 . 7))))
(4 . ((1 . (1 . 4)) (1 . (3 . 4)) (1 . (4 . 5)) (1 . (4 . 7))))
(5 . ((2 . (1 . 5)) (2 . (3 . 5)) (1 . (4 . 5)) (5 . (5 . 9))))
(6 . ((1 . (2 . 6)) (8 . (6 . 7))))
(7 . ((2 . (1 . 7)) (2 . (3 . 7)) (1 . (4 . 7)) (8 . (6 . 7))))
(8 . ((5 . (1 . 8)) (6 . (2 . 8)) (2 . (8 . 9))))
(9 . ((5 . (2 . 9)) (5 . (5 . 9)) (2 . (8 . 9))))))
Referências
- ↑ Segaran, Toby (2007). Programming Collective Intelligence First ed. [S.l.]: O’Reilly. ISBN 0-596-52932-5
- ↑ Kleinberg, Jon; Tardos, Éva (2006). «4: Greedy Alorithms». Algorithm Design First ed. [S.l.]: Cornell University. ISBN 0-262-03293-7