TProcura
Biblioteca em C++ para testes paramétricos de algoritmos, e coleção de algoritmos de procura e otimização
|
Execução de exemplo com base na classe de teste de TVector. Pode acompanhar o teste executando as ações localmente. Este exemplo serve para ilustrar as funcionalidades principais de testes paramétricos da classe TProcura. Nos exemplos das subclasses ilustra-se as restantes funcionalidades.
Ao arrancar com o programa sem argumentos, entramos no modo interativo, com o teste manual. Esta é a informação apresentada. Na zona superior aparece o nome do problema (neste caso TVector), seguido dos parametros e valores atuais. Iremos detalhar os parametrors mais adiante. Segue-se uma zona com informação sobre os dados do problema concreto a resolver, a instância, neste caso é um vetor de números aleatórios, com 1 milhão de elementos. Temos vários algoritmos, em que cada um testa um método da classe TVector. Segue-se o menu com 8 comandos, os quais iremos cobrir neste exemplo. Entre os dados e o menu, após ter exsitido uma execução, são apresentados os indicadores relativos à última execução. Iremos detalhar também estes indicadores.
Vamos ver o menu 1, inicializar. Introduza: 1; 2.
Este menu permite inicializar os dados utilizados no algoritmo. Temos a indicação do ID da instância atual, que é 1. Estão definidas instâncias de 1 a 10. Essas instâncias significam que o tamanho do vetor é 1 milhão vezes o ID da instância. Ao escolhermos a instância 2, vemos que o vetor fica com 2 milhões de dados. Fizemos a visualização apenas dos primeiros 3 e últimos 3 elementos, mas neste caso é suficiente para sabermos que trocamos de instância, já que os números são diferentes. São diferentes os últimos números, mas os primeiros são iguais, já que não alteramos a semente do gerador aleatório. Podiamos também ter introduzido um texto, para alterar o prefixo atual. Este texto é importante se os dados de teste estivessem em ficheiros. Neste caso fizemos um gerador de dados, geramos os dados aleatoriamente e colocamos no vetor, não é preciso o ficheiro.
Vamos voltar para a instãncia 1, para confirmar que é a mesma, apenas assim se pode garantir que tudo o que se faz em TProcura, é reproduzivel. Introduza: 1; 1.
Notar que a instância é exatamente a mesma, como se pode confirmar pelos números iniciais e finais do vetor.
O menu 2 - Explorar, serve para explorar o problema manualmente, mas neste caso não definimos nenhuma função para expplorar os dados. Esta função está definida para as subclasses de TProcura, para os métodos construtivos e melhorativos. Não é necessário definir função nenhuma, e o utilizador pode sempre explorar manualmente o problema, por que não, tentar resolvê-lo, e assim ganhar sensibilidade. Vamos deixar esta exploração para os exemplos das subclasses.
Notar que o menu no modo interativo, é sempre visualizado, colocando-se o prompt "Opção:". Para evitar repetição, vamos omitar o menu no resto deste exemplo.
Avançamos para o menu 3 - Parâmetros. Introduza: 3.
Podemos ver os parametros quer já tinhamos visto, com mais detalhe. Temos o ID do parâmetro, nome do parâmetro, e valor atribuÃdo. Alguns parâmetros têm texto, quando as opções são categóricas, outros têm números, para valores quantitativos. Não existem números reais nos parâmetros, tendo de ser convertidos sempre para inteiro. Mesmo os parâmetros categóricos, têm um número inteiro associado a cada valor. Cada parâmetro tem um valor mÃnimo e máximo em inteiros, que pode tomar.
Vamos ver as opções no parâmetro 1, é o método a ser executado. Introduza: 1.
Como o parâmetro é catégórico, aparecem todos os valores que pode tomar, juntamente com os seus nomes. Este parâmetro foi povoado na classe CTesteTVector, onde foram definidos estes métodos. A função do parâmetro 1 é sempre escolher o método/algoritmo a executar, mas os métodos/algoritmos variam conforme a subclasse. Ao utilizar uma das subclasses genéricas de TProcura, esta já tem um conjunto de algoritmos implementados, ficando esta lista preenchida.
Caso não pretenda alterar um parâmetro, pode sempre carregar em ENTER para manter o valor atual. Vamos alterar para a ordenação, e de seguida vamos ver o parâmetro 2. Introduza: 2; 2.
Notar que agora o valor associado a P1 é "Sort()". Ao escolhermos o parâmetro P2 vamos ver o parâmetro que define o nÃvel de debug. Este parâmetro é de TProcura, e não é necessário alterar, a não ser que se pretenda mais que 3 nÃveis de debug.
Ao alterar este parâmetro, o algoritmo, caso tenha condicionais sobre este parâmetro, irá mostrar os detalhes do que está a fazer. Prevê-se estes nÃveis, em que o nÃvel 1 deve apresentar apenas um caracter de tempos a tempos, de modo a confirmar-se que o algoritmo está a funcionar. No nÃvel 2 deve-se dar a indicação do passo/iteração em que vai, ou grande passo, no caso do número de iterações ser elevado. No nÃvel 3 detalhe, já é de esperar detalhe sobre todas as iterações. No nivel 4 completo, todo o detalhe deve ser dado em todos os passos, de modo a constituir uma prova de que o resultado é correto.
Estes nÃveis têm o intuito de poder observar bugs, caso existam. Mas têm também um carácter didático, já que nas classes de procura, pode-se observar os algoritmos com o nÃvel que se pretender. Assim está-se a contribuir para a compreensão do algoritmo, aplicado a um dado problema.
No caso do TVector, os algoritmos estão implementados sem debug, pelo que este valor não é utilizado.
Avançamos para o parâmetro P3, "Seed". Introduza: ENTER; 3; 2; ENTER.
O P3 tem a semente aleatória. No entanto, voltamos ao menu inicial e a instância é a mesma. É preciso inicializar e com esta nova semente, já irá ser criada uma nova instância. Introduza: 1; ENTER.
Agora sim, podemos ver que os três primeiros e últimos números são completamente distintos.
Os outros dois parâmetros, o P4 Tempo, tem o tempo em segundos para o algoritmo executar, e P5 Iterações, é um parâmetro genérico para limitar o número de iterações. Como não temos nestes algoritmos definidas iterações, não iremos utilizar.
O P6 é um parâmetro definido na classe CTesteTVector, e tem a estrutura em teste. Introduza: 3; 6.
Foram definidas 3 opções pela qual os métodos podem ser executados. Utilizando exclusivamente a TVector, a opção atual. A segunda opção é não utilizando TVector, mas sim std::vector e algoritmos da STL equivalentes aos métodos em teste de TVector. A terceira opção é um misto entre utilizar TVector, mas na ordenação e deteção de estados únicos, utilizar os métodos da STL.
Vamos deixar como está. Introduza: ENTER; ENTER.
O menu 4 - Solução será ilustrado com um problema de procura, nestes métodos a solução é o estado do vetor após aplicadas as operações.
Avançamos para o menu 6 - Executar. Introduza: 6.
Os valores dos parâmetros em modo curso são mostrados, e de seguida a execução é completada. Podemos ver novamente os parâmetros utilizados, tendo sido executado o método Sort(), com a estrutura TVector. O estado do vetor é visivel os três primeiros e últimos elementos, podendo-se confirmar que estes estão por ordem.
Como já houve uma execução, existe agora mais uma linha antes do menu, com os indicadors, I1 a I4.
Os três primeiros indicadores são de TProcura, o quarto indicador é definido em CTesteTVector. O indicador I1 tem o resultado do algoritmo, que aqui retorna sempre 1 a não ser que exista algum problema. O indicador I2 tem o tempo consumido pelo algoritmo, em milisegundos, neste caso 82. Notar que este tempo é apenas de execução, não é contabilizado o tempo de inicialização (ao criar a instância). O tempo de calcular os indicadores não é também comtabilizado. O indicador I3 tem o número de iterações realizadas. O algoritmo deve atualizar as iterações, neste caso ordenou apenas uma vez. Como queremos saber se a operação foi bem sucedida, definiu-se um indicador para testar se o vetor está ordenado. Esse indicador é chamado após o algoritmo, e aqui retorna 1, confirmando que está ordenado. Em outros métodos, que não ordenem o vetor, naturalemnte que este indiador ao ser chamado, irá retornar 0.
Vamos trocar um parâmetro, inicializar e executar novamente. Introduza: 3; 6; 2; ENTER; 1; ENTER; 6.
Notar que o resultado em termos de dados final, é exatamente o mesmo. É natural já que ambas as estruturas de dados receberam a mesma instância. O tempo foi um milisegundo superior, mas outra execução o tempo CPU pode ser distinto.
Vamos agora ver o menu dos indicadores. Introduza: 5.
De omissão estão todos os indicadores ligados, e são chamados por ordem de ID. No entanto, por vezes pretende-se verificar a solução, executando verificações/validações. Outras vezes pretende-se obter informação sobre a instância, que nem está relacionada com a execução do algoritmo. Pode-se ainda querer fazer ações após o algoritmo, de transformação da solução, ou gravação da solução para ficheiro.
Com os indicadores é possÃvel definir as ações a executar após o algoritmo, por uma dada ordem, ou se uma dada ação irá ser executada ou não.
Neste caso temos apenas um indicador definido, que é um indicador de verificação, testando se o vetor ficou ordenado.
Com esta interface podemos remover e adicionar indicadores existentes por outra ordem. Vamos por exemplo colocar visivel primeiro I4 e depois I2, não mostrando os restanes.
Introduza: 1;2;3;2;ENTER;1;ENTER;6.
Podemos ver que agora apenas I4 seguido de I2 são apresentados.
Os métodos utilizados até aqui, permitem executar testes confortavelmente sem ter de andar a alterar código, compilar e executar. Pode-se ir registando os dados, e procurar tirar conclusões.
No entanto, execuções particulares podem ser enganadoras, como o tempo de execução da ordenação entre TVector e o sort() da STL. Para existir garantias, temos de fazer muitas execuções, com diferentes parâmetros, e poder processar os resultados posteriormente.
Assim, é crÃtico que se possa definir confortavelmente todas as configurações a executar, de modo a obter resultados em bloco, e não um a um.
Introduza: 7.
Temos aqui a lista dos parâmetros atuais. Há apenas uma configuração, com os parâmetros que alteramos. Se não alterarmos parâmetros, estes tomam o valor de omissão, nunca existindo um parâmetro sem valor definido. Os parâmetros apenas podem ser alterados para valores dentro dos seus limites.
Existe atualmente apenas uma configuração, em que todos os parâmetros são comuns. Vamos colocar o parâmetro P1, com o método, a variar de 1 a 12, para testar todos os métodos.
Introduza: P1=1:12
Notar que P1 já não é mostrado nos parâmetros comuns, mas sim os seus diversos valores nas configurações de 1 a 12. A notação 1:12 significa os números de 1 a 12. Se tivessemos colocado 1:12:2 correspondia aos números de 1 a 12 mas em passos de 2. Podiamos ter indicado também com a notação por vÃrgulas, com 1,2,3,4,5,6,7,8,9,10,11,12. Podemos misturar notações, por exemplo, podiamos especificar o conjunto 1:3,5:9:2,12, ou seja, os números 1,2,3,5,7,9,12.
Se pretendermos alterar apenas o valor de um parâmetro k para V, colocamos Pk=V. Se tiver dois ou mais números, então pretendemos variar a configuração atual.
Vamos agora colocar dois parâmetros a variar em conjunto, ou seja, estamos interessados no produto externo. Introduza: P3=1:2 x P6=1:3.
Foram dados 2 valores para P3 e 3 valores para P6, tendo sido produzidos mais 5 novas configurações, já que uma é a configuração base (a atual).
Vamos agora utilizar as configurações num teste. Introduza: ENTER.
Avance para a próxima ação.
Tendo configurações introduzidas, vamos agora executar um teste com estas configurações. Introduza: 8; 1:3; ENTER; 0.
A primeira pergunta é sobre quais as instâncias a excutar o teste, na qual pretendemos as 3 primeiras. A segunda pergunta é para colocar os resultados num ficheiro de texto, para já não estamos interesados. A terceira pergunta permite que se vejam as soluções assim que cada execução termina, também não precisamos neste teste.
Podemos ver que durante a execução, é mostrada a configuração atual, a instância a ser resolvida. No final são mostrados resultados para cada configuração e instância, agregasdos por configuração. Os indicadores selecionados fazem parte da tabela de resultados.
Podemos ver que o indicador definido na subclasse, Ordenado, retorna 1 nos métodos cujo resultado final o vetor fica ordenado, e 0 nos restantes.
É mostrado ainda um torneio entre configurações, para saber a que é mais rápida. Para permitir a reprodução e identificar eventuais enganos, é mostrado no final os valores exatos de cada configuração.
Infelizmente, pouco podemos deduzir deste teste. O motivo é que não tinhamos uma pergunta formulada, mas solicitamos simplesmente execuções com diversas configurações. Sabemos apenas que não há crash.
Vamos formular uma pergunta. Pretendemos saber como varia o tempo na operação de ordenação, com o tamanho do vetor.
Poderiamos estar a apagar as configurações uma a uma, mas vamos arrancar de novo, para não ter qualquer configuração. Introduza após arrancar: 7; P1=2; ENTER; 8; 1:10; ENTER; 0.
Podemos ver que o tempo sobe, mas não de forma linear. A instância 1 tem 1 milhão de valores, leva 84 milisegundos. A instância 10 tem 10 vezes mais tamanho, mas leva mais que 840 milisegundos, foi 1314. Foram execuções únicas, os valores podem variar demasiado.
Vamos colocar 10 instâncias de cada tipo, variando a semente, para assim ter mais precisão. Queremos desta vez ter os resultados no Excel.
Introduza: 7; P3=1:10; ENTER; 8; 1:10; resultado; 0.
Existe a indicação que o ficheiro resultado.csv foi gravado.
O ficheiro está pronto para fazermos uma análise com os relatórios dinâmcios. Colocando a instância nas linhas, e o tempo no conteúdo, deverá ser visivel o tempo crescente de forma estável, um pouco acima de linear.
Embora o modo interativo seja útil no desenvolvimento do algoritmo ou problema, os testes paramétricos é que nos vão suportar uma resposta a algum tipo de hipótese formada.
A linha de comando é normalmente mais simples, para por um lado poder colocar a correr num servidor, por outro, pode ver exatamente o teste solicitado numa só linha, não estando dependente da interação do utilizador.
Abra a linha de comando, localize o executável, e execute com argumento "-h"
A lista completa de parametros e indicadores é também mostrada, para permitir saber o que se pode utilizar na definição das configurações de execução.
Podemos assim reproduzir o teste anterior com a seguinte entrada.
Linha de comando: TProcura.exe 1:10 -R resultado2 -P P1=2 P3=1:10
Podemos agora processar o ficheiro e confirmar que tem resultados idênticos, com ligeiras variações no tempo.
Estando respondida a questão inicial sobre o tempo de ordenação, podemos com este código procurar responder a outra questão:
De modo a ter uma resposta rápida mas com várias iteraçoes, vamos utilizar apenas as instâncias 1 e 3, mas mantendo 10 sementes aleatórias. Temos também de variar a estrutura de dados.
Linha de comando: TProcura 1,3 -R resultadosTudo -P P1=1:12 x P3=1:10 x P6=1:3
Podemos processar no relatório dinâmico, colocando nas linhas P1 com os métodos, nas colunas P6 com as estruturas, e no conteúdo I2 com o tempo.
Segundo estes resultados, TVector tem uma ligeira vantagem em termos de tempo na ordenação, mas é pior na união e outras operações, e no final há uma diferença de 1 segundo em 35. Foram utilizadas instãncias pequenas para que o teste possa ser rápido. Em qualquer caso pode-se afirmar que não há uma perda muito grande por utilizar TVector em vez do código STL.
Um ficheiro script com a linha de comandos, tem toda a informação para reproduzir o teste, pelo que pode facilitar a identificação do que foi feito. Por outro lado, o ficheiro de resultados tem também todos os valores utilizados, pelo que se houve algum engano na especificação dos parâmetros, o valor utilizado incorreto é visivel nos resultados. Há uma clara separação da fase de implementação da fase de teste. O resultado de um teste pode levantar outras questões, e provocar outro teste. Se a implementação tiver todas as opções em parâmetros, não é necessário alternar com programação entre testes. Apenas após a identificação de bugs, é que a programação é necessária.
Será a ordenação de TVector mais rápida ou mais lenta ue a ordenação da STL? Que testes pode fazer para procurar dar uma resposta fundamentada?
Deve procurar executar com instâncias de dimensão razoável, vamos utilziar a 10 que é a maior. Deve-se executar várias vezes, e apenas na operação de ordenação. Linha de comando: TProcura 10 -R resultadosSort -P P1=2 P3=1:10 x P6=1:3 Os resultados aparentam confirmar que há uma certa vantagem para o algoritmo, certamente mais simples do TVector nestes vetores. Um teste estatÃstico poderá dar resposta se as médias são distintas ou não, mas tudo indica que sim. Este pode ser um efeito de uma codificação intensa do STL não beneficiar de optimizações que tenham sido introduzidas no compilador, ao contrário de um código que seja compilado na altura.