algoritmos de programaÇÃo dinÂmica usados em
TRANSCRIPT
INPE-13052-TDI/1021
ALGORITMOS DE PROGRAMAÇÃO DINÂMICA USADOS EM MODELOS MARKOVIANOS OCULTOS (HMMs)
José Olimpio Ferreira
Tese de Doutorado do Curso de Pós-Graduação em Computação Aplicada, orientada pelo Dr. Solon Venâncio de Carvalho, aprovada em 29 de março de 2004.
INPE São José dos Campos
2005
519.8 FERREIRA, J. O. Algoritmos de programação dinâmica usados em modelos markovianos ocultos (HMMs) / J. O. Ferreira. – São José dos Campos: INPE, 2004. 250p. – (INPE-13052-TDI/1021). 1.Processos estocásticos. 2.Modelos markovianos. 3.Progrramação dinâmica. 4.Modelos Markovianos Ocultos (HMMs). 5.Biologia computacional. 6.Bioinformática. I.Título.
O engajamento na ciência (empregando a palavra na acepção geral alemã Wissenschaft) não reside na discordância sobre fatos verificados, mas sobre sua
escolha e combinação, e sobre o que se pode inferir a partir dos mesmos.
ERIC HOBSBAWN (1917-), historiador, em “Sobre Historia”.
E para que executar projetos, já que o projeto é, nele mesmo, um prazer suficiente?
CHARLES BAUDELAIRE (1821-1867), poeta francês, em “Pequenos poemas em prosa”.
Dedico carinhosamente esta dissertação a meus pais MANOEL e ELIZENA ( )
a meus filhos VITOR, VINICIUS e NATÁLIA a minha esposa MARIA das GRAÇAS
e àqueles que me incentivaram e apoiaram.
AGRADECIMENTOS
Agradeço a todas pessoas que me ajudaram a vencer mais essa etapa da vida. Agradeço à Fundação de Aperfeiçoamento de Pessoal de Nível Superior - CAPES, pelo auxilio financeiro. Agradeço so Instituto Nacional de Pesquisas Espaciais - INPE pela oportunidade de estudos e utilização de suas instalações. Agradeço aos professores do INPE pelo conhecimento compartilhado. Agradeço à Universidade Católica de Goiás – UCG - por ter proporcionado as condições físicas e financeiras para que eu pudesse cursar o Doutorado em Computação Aplicada no Instituto Nacional de Pesquisas Espaciais – INPE. Agradeço ao meu orientador Dr. Solon Venâncio de Carvalho pela sua imensa paciência e valioso incentivo na realização desta pesquisa e na confecção deste relatório. Agradeço ao apoio de minha esposa Maria das Graças, de minha filha Natália e dos meus filhos Vitor e Vinicius que conviveram durante esse período com a minha ausência, com as minhas angústias, ansiedades e o meu nervosismo. Agradeço a meus pais por sempre acreditarem na importância do estudo. Agradeço, também, aos amigos e colegas que me apoiaram e incentivaram a fazer esse curso de Doutorado em Computação Aplicada, e que me aconselharam nos momentos mais delicados.
RESUMO
Esta tese trata dos algoritmos de programação dinâmica que são usados nos Modelos Markovianos Ocultos (HMMs), perfis-HMMs, aplicados no estudo de seqüências biológicas no campo da Bioinformática. O foco é a investigação de técnicas (métodos ou paradigmas) de economia de espaço que proporcionem a melhor economia de tempo, e que sejam adequadas para a utilização no cálculo de medidas de interesse dos HMMs. Explorou-se a hipótese da utilização da estratégia de checkpoints em conjunto com o princípio D&C como solução consistente para o problema de complexidade de espaço versus complexidade de tempo dos algoritmos de programação dinâmica utilizados no cálculo de medidas de interesse em perfis-HMMs. Propõe-se um algoritmo denominado algoritmo de programação dinâmica com L-níveis de checkpoints bidimensionais que pode ser usado em conjunto com procedimentos de retrocedimento parcial ou completo, sobre a matriz de programação dinâmica. A versão com retrocedimento parcial desse algoritmo proposto, denominada algoritmo de Viterbi com L-níveis de checkpoints bidimensionais, com partição fixa de memória e retrocedimento restrito, foi superior em desempenho, tanto na análise teórica, quanto nos testes de desempenho a posteriori, ao algoritmo de Viterbi com L-níveis de checkpoints por diagonais, com partição móvel de memória e retrocedimento restrito, considerado o estado da arte entre esses algoritmos de programação dinâmica com a técnica de checkpoints. O desempenho desse algoritmo proposto foi superior, nos testes a posteriori, inclusive ao próprio algoritmo de Viterbi básico. Na simulação dos requerimentos de memória, verificou-se que os requerimentos de memória desse algoritmo proposto são uma fração decrescente dos requerimentos de memória do algorimo de Viterbi básico, para instâncias crescentes do problema, indicando um comportamento assintótico dos requerimentos de memória muito favorável para a computação de instâncias do problema que são intratáveis pelo algoritmo de Viterbi básico.
DYNAMIC PROGRAMMING ALGORITHMS USED IN HIDDEN MARKOV MODELS (HMMs)
ABSTRACT
This thesis is about dynamic programming algorithms used in Hidden Markov Models (HMMs) -HMM profiles - applied to biological sequence studies in bioinformatics. Its investigation focuses on space economy techniques (methods or paradigms), that provide best time-saving solutions, which would be adequate for use in calculating HMM interest measures. The checkpoints paradigm use hypothesis together with the D&C strategy, as a consistent solution for the problem of space versus time complexity in dynamic programming algorithms which are used in the calculation of measures of interest to the HMM profiles, is explored. An algorithm denominated dynamic programming algorithm with L-levels of bidimensional checkpoints which can be used together with partial or complete traceback procedures through to the dynamic programming matrix is proposed. The version with partial traceback denominated the Viterbi algorithm with L-levels of bidimensional checkpoints with fixed memory partition and restricted traceback was superior in performance to the Viterbi algorithm with L-levels of diagonals checkpoints with mobile memory partition and restricted traceback, not only in the theoretical analysis but also in the tests a posteriori. The latter is considered state of the art among the dynamic programming algorithms with the checkpoints technique. In the tests a posteriori, the proposed algorithm has been proved to be superior in performance even to the basic Viterbi algorithm. In the memory requirement simulation, the memory requirements of the new algorithms have been shown to be a decreasing fraction of those of the Viterbi basic algorithm for growing instances of the problem, indicating an asymptotic behavior of memory requeriments, very favorable to the computation of instances of the problem which are intractable by the basic Viterbi algorithm.
SUMÁRIO
Pág.
LISTA DE FIGURAS ....................................................................................................17
LISTA DE TABELAS ...................................................................................................19
LISTA DE SÍMBOLOS .................................................................................................21
LISTA DE SIGLAS E ABREVIATURAS ....................................................................25
CAPÍTULO 1 INTRODUÇÃO...................................................................................27
1.1 Introdução .......................................................................................................27
1.2 O Problema......................................................................................................38
1.3 Objetivos e Hipóteses ......................................................................................40
1.4 Justificativa......................................................................................................41
CAPÍTULO 2 CONCEITOS BÁSICOS.....................................................................47
2.1 Introdução .......................................................................................................47
2.2 Modelos Markovianos Ocultos (HMMs) ........................................................47
2.2.1 Definições .......................................................................................................47
2.2.2 Os Três Problemas Básicos dos HMMs ..........................................................49
2.2.2.1 Soluções para o Problema 1 ............................................................................49
2.2.2.2 Soluções para o Problema 2 ............................................................................51
2.2.2.3 Soluções para o Problema 3 ............................................................................55
2.2.3 Modelo de Atribuição de Escores e Problemas Numéricos ......... ..................57
2.2.4 Perfis-HMMs para Seqüências Biológicas ......................................................60
2.3 Medidas de Complexidade de Algoritmos .....................................................68
2.3.1 Definição de Algoritmo...................................................................................68
2.3.2 Análise de Desempenho de Algoritmos e Complexidade de Problemas ........70
2.3.2.1 Análise Assintótica ..........................................................................................75
2.4 Algoritmos de Programação Dinâmica............................................................78
2.4.1 Estratégia de Programação Dinâmica..............................................................78
2.4.2 Métodos de Programação Dinâmica e HMMS................................................79
CAPÍTULO 3 ALGORITMOS DE PROGRAMAÇÃO DINÂMICA COM
CHECKPOINTS USADOS EM PERFIS-HMMS...............................83
3.1 Introdução .......................................................................................................83
3.2 Revisão da Literatura.......................................................................................84
3.3 Algoritmos de Programação Dinâmica com Checkpoints..............................102
3.3.1 Algoritmo A: 2-Níveis com Partição Fixa.....................................................105
3.3.2 Algoritmo B: 2-Níveis com Partição Móvel .................................................106
3.3.3 Algoritmo C: 3-Níveis com Partição Móvel .................................................108
3.3.4 Algoritmo D: L-Níveis com Partição Móvel .................................................109
3.3.5 Algoritmo E: Viterbi com L-Níveis de Checkpoints por Diagonais com Retrocedimento Restrito ................................................................................112
3.3.6 Disposição Ótima de Checkpoints por Linhas (Colunas) e o Algoritmo Melhorado .....................................................................................................118
3.3.6.1 Algoritmo Melhorado....................................................................................119
3.3.6.2 Algoritmo Ótimo ...........................................................................................120
3.3.7 Disposição Ótima de Checkpoints por Diagonais e o Algoritmo de Viterbi Melhorado .....................................................................................................121
CAPÍTULO 4 ALGORITMOS PROPOSTOS .........................................................123
4.1 Introdução .....................................................................................................123
4.2 Algoritmos de L-Níveis de Checkpoints Bidimensionais..............................124
4.2.1 Algoritmo F: 2-Níveis de Checkpoints Bidimensionais e Partição Fixa de Memória ....................................................................................................130
4.2.2 Algoritmo G: 3-Níveis de Checkpoints Bidimensionais e Partição Fixa de Memória ....................................................................................................132
4.2.3 Algoritmo H: L-Níveis de Checkpoints Bidimensionais e Partição Fixa de Memória ....................................................................................................135
4.2.4 Algoritmo J: Viterbi com L-Níveis de Checkpoints Bidimensionais e Partição Fixa de Memória............................................................................................136
CAPÍTULO 5 EXPERIMENTAÇÃO E ANÁLISE DOS RESULTADOS.............145
5.1 Introdução .....................................................................................................145
5.2 Metodologia de Experimentação...................................................................145
5.3 Implementação dos Algoritmos e Realização dos Experimentos .................150
5.4 Apresentação e Análise dos Resultados da Experimentação.........................161
CAPÍTULO 6 CONCLUSÕES E COMENTÁRIOS................................................185
REFERÊNCIAS BIBLIOGRÁFICAS .........................................................................201
BIBLIOGRAFIA COMPLEMENTAR …...................................................................207
APÊNDICE 1 - ALGORITMO DE VITERBI BÁSICO .............................................209
APÊNDICE 2 - ALGORITMO DE VITERBI COM DE 2-NÍVEIS CHECKPOINTS POR LINHAS E PARTICIONAMENTO MÓVEL DE MEMÓRIA.217
APÊNDICE 3 - ALGORITMO DE VITERBI COM 2-NÍVEIS DE CHECKPOINTS POR DIAGONAIS, COM PARTICIONAMENTO MÓVEL DE MEMÓRIA E RETROCEDIMENTO RESTRITO...........................227
APÊNDICE 4 - ALGORITMO DE VITERBI COM 2-NÍVEIS DE CHECKPOINTS BIDIMENSIONAIS, PARTICIONAMENTO FIXO DE MEMÓRIA E RETROCEDIMENTO RESTRITO ..................................................239
GLOSSÁRIO ...............................................................................................................249
LISTA DE FIGURAS
2.1 - HMM discreto com três estados e 5 símbolos de observação................................ 49
2.2 - Topologia de um perfil-HMM com 4 módulos padrões (Hughey e Krogh, 1996).62
2.3 – Taxa de crescimento do tempo de computação de funções típicas. ...................... 78
4.1 - Particionamento A: L-níveis de checkpoints completos e particionamento fixo de memória. .............................................................................................................. 125
4.2 - Particionamento B: L-níveis de checkpoints completos e particionamento fixo de memória. .............................................................................................................. 125
4.3 - Particionamento C: L-níveis de checkpoints completos e particionamento móvel de memória. .............................................................................................................. 126
4.4 - Particionamento D: L-níveis de checkpoints parciais e particionamento fixo de memória. .............................................................................................................. 126
4.5 - Particionamento E: L-níveis de checkpoints parciais e particionamento móvel de memória. .............................................................................................................. 127
4.6 - Particionamento F: L-níveis de checkpoints parciais e particionamento fixo de memória. .............................................................................................................. 127
4.7 - Particionamento G: L-níveis de checkpoints parciais e particionamento móvel de memória. .............................................................................................................. 128
5.1 - Distribuição do tamanho das seqüências de proteínas – adaptada de figura do Swiss-Prot group (2004)...................................................................................... 146
5.2 - Modelo Nulo, R, totalmente probabilístico. ......................................................... 153
5.3 – Tempos de CPU dos algoritmos Viterbi para alinhamento de seqüências de aminoácidos a um HMM com 2000 nós.............................................................. 168
5.4 – Tempos totais de execução dos algoritmos de Viterbi para alinhamentos de seqüências de aminoácidos a um HMM com 2000 nós....................................... 169
5.5 – Tempos de CPU dos algoritmos de Viterbi para alinhamentos de seqüências de aminoácidos a um HMM com 2000 nós (eixos logarítmicos)............................. 170
5.6 – Tempos totais de execução dos algoritmos de Viterbi para alinhamentos de seqüências de aminoácidos a um HMM com 2000 nós (eixos logarítmicos). .... 171
5.7 – Tempos de CPU dos algoritmos Viterbi para alinhamento de seqüências de aminoácidos a um HMM com 2000 nós.............................................................. 172
5.8 – Tempos totais de execução dos algoritmos Viterbi para alinhamento de seqüências de aminoácidos a um HMM com 2000 nós......................................................... 173
5.9 – Tempos de CPU para efetuar 4 execuções de computações de programação dinâmica de seqüências de aminoácidos contra um HMM com 500 nós – extraído da Figura 3 de Tarnas e Hughey (1998). ............................................................. 174
5.10 – Tempos totais de execução para efetuar 4 execuções de computações de programação dinâmica de seqüências de aminoácidos contra um HMM com 500 nós – extraída da Figura 3 de Tarnas e Hughey (1998)....................................... 175
5.11 – Tempos de CPU dos algoritmos Viterbi para alinhamento de seqüências de aminoácidos a um HMM com 368 nós................................................................ 177
5.12 – Tempos totais de execução dos algoritmos Viterbi para alinhamento de seqüências de aminoácidos a um HMM com 368 nós......................................... 178
LISTA DE TABELAS
2.1 - Taxas de Crescimento Típicas................................................................................77
3.1 - Resumo do Desempenho de Alguns Algoritmos de Espaço Linear para o Problema LCS........................................................................................................................90
5.1 - Códigos dos 20 Aminoácidos, com Uma e Três Letras. ......................................152
5.2 - Freqüências Dayhoff de Ocorrências de Aminoácidos do Banco de Dados Swissprot 34 ........................................................................................................153
5.3 - Resultados das Estimativas dos Requerimentos de Memória para os Algoritmos de Viterbi (Valores em Mb).....................................................................................163
5.4 - Resultados das Estimativas Normalizadas dos Requerimentos de Memória para os Algoritmos de Viterbi (Básico = 1). ...................................................................164
5.5 - Resultados dos Tempos de CPU e dos Tempos Totais de Execução (Wall Time) do Primeiro Experimento para os Algoritmos de Viterbi (Tempos em Segundos). .165
5.6 - Resultados dos Tempos de CPU e dos Tempos Totais de Execução (Wall Time) do Segundo Experimento para os Algoritmos de Viterbi (Tempos em Segundos). .167
5.7 - Resultados dos Tempos de CPU e dos Tempos Totais de Execução (Wall Time) do Terceiro Experimento para os Algoritmos de Viterbi (Tempos em Segundos). .176
LISTA DE SÍMBOLOS
A - Matriz de probabilidades de transição de estados
A - Matriz de estimativas das probabilidades de transição de estados
(Aij) - Número de vezes que cada transição ocorre no conjunto de observações de treinamento
aij - Probabilidade de transição do estado i para o estado j
ija - Estimativa da probabilidade de transição do estado i para o estado j
bt(i) - Variável backward, definida como a probabilidade de um certo sufixo até um determinado estado, dado o modelo λ
( )nRC , - Quantidade mínima de computações de linhas da matriz de PD ou o custo de computar n linhas da matriz de PD com R linhas de memória
c - Distância entre dois checkpoints sucessivos
Dj - O j-ésimo estado D (delete)
d - Número de pares de pontos concordantes + indels, ou valor do escore
E - Matriz de probabilidades de emissão de símbolos
E - Matriz de estimativas das probabilidades de emissão de símbolos
(Ei(k)) - Número de vezes que cada emissão ocorre no conjunto de observações de treinamento
ie - Estimativa da probabilidade de emissão do símbolo k no estado j
ej(k) - Probabilidade de emissão do símbolo k no estado j
)(tF Dj - Escore forward em log-odds dos caminhos terminando com Oi sendo
emitido pelo estado Dj )(tF I
j - Escore forward em log-odds dos caminhos terminando com Oi sendo emitido pelo estado Ij
)(tF Mj - Escore forward em log-odds dos caminhos terminando com Oi sendo
emitido pelo estado Mj ft(i) - Variável forward, definida como a probabilidade de um certo prefixo até
um determinado estado, dado o modelo λG(vk|O) - Probabilidade a posteriori do símbolo vk vir de um estado num conjunto
especificado Ij - O j-ésimo estado I (insert)
K - Número de símbolos de um alfabeto discreto e finito
L - Número de níveis de checkpoints
l - Operação elementar
Mj - O j-ésimo estado M (match)
m - Comprimento de uma dada string ou seqüência de observações
N - Número de estados
n - Comprimento do HMM ou de uma dada string ou seqüência de observações
O - Seqüência de observações
O(n) - Notação big-O para comparação do crescimento assintótico de funções
o(n) - Notação little-o para comparação do crescimento assintótico de funções
P(O/λ) - Probabilidade de ocorrência de uma certa seqüência de observações O dado o modelo λ
p - Tamanho da LCS
Q - Seqüência ou caminho de Estados de um HMM
Q* - Caminho de estados mais provável
qt - Processo estocástico que indica o estado do modelo no instante t
q - Número de pontos concordantes dominantes
R - Número de linhas (colunas ou diagonais) da matriz de PD que podem ser armazenadas como checkpoints
Rbd - Linhas de memória de checkpoints bidimensionais
Rcp - Linhas de memória R destinadas a armazenar os checkpoints
Rtb - Linhas de memória R destinadas ao cálculo de seções da matriz de PD
( )RrL - Número máximo de linhas da matriz de PD, que podem ser computadas com o algoritmo de L-níveis
r - Número de pares de pontos concordantes
S - Conjunto de estados de um HMM
Si - i-ésimo estado do HMM
( )nSL - Espaço necessário para armazenar os checkpoints de nível L
s - Tamanho do alfabeto
T - Número de épocas ou tempo
( )nTL - Estimativa do tempo total gasto pelo algoritmo com L-níveis de checkpoints
( )tTBXj - Vetor para guardar o argumento que maximiza V , , )(tXj MIDX ,,∈
para t e jt - Instante de tempo (época)
V - Conjunto de símbolos que constitui um alfabeto discreto e finito
)(tV Dj - Escore Viterbi em log-odds do melhor caminho terminando com Oi
sendo emitido pelo estado Dj )(tV I
j - Escore Viterbi em log-odds do melhor caminho terminando com Oi sendo emitido pelo estado Ij
)(tV Mj - Escore Viterbi em log-odds do melhor caminho terminando com Oi
sendo emitido pelo estado Mj vi - i-ésimo símbolo de um conjunto de símbolos que constitui um alfabeto
discreto e finito vt(i) - Variável Viterbi, definida como a mais alta pontuação (mais alta
probabilidade) ao longo de um caminho até o instante t w - Tamanho das palavras da máquina (8, 16, 32, ou 64 bits)
γt(i) - Probabilidade a posteriori do estado Si no instante t,
λ - Parâmetros do modelo, também, designa o próprio modelo
λ - Estimativa dos parâmetros do modelo
π - Vetor de probabilidades para o estado inicial de um HMM
π - Vetor de estimativas das probabilidades para o estado inicial de um HMM
πi - Probabilidade para o estado inicial i de um HMM
iπ - Estimativa da probabilidade para o estado inicial i de um HMM
ψt(j) - Vetor para guardar, para cada t e j, o argumento que maximiza vt(i)
))(( nfΩ - Notação big-Ω para comparação do crescimento assintótico de funções
))(( nfω - Notação little-ω para comparação do crescimento assintótico de funções
))(( nfΘ
- Notação big-Θ para comparação do crescimento assintótico de funções
Σ - Alfabeto composto pelos símbolos dos aminoácidos (proteínas)
Σd - Alfabeto degenerado para as proteínas
LISTA DE SIGLAS E ABREVIATURAS
ADN - Ácidos desoxirribonucléicos
ANSI - American National Standards Institute
API - Interface de programação da aplicação (application program interface)
ARN - Ácidos ribonucléicos
ARNm - Ácidos ribonucléicos mensageiros
bp - Pares de bases
CPU - Unidade de processamento central
D&C - Dividir e conquistar
ECG - Eletrocardiograma
GCC - Coleção de compiladores da GNU
GNU - a recursive acronym for "GNU's Not Unix"
HMMpro - Pacote de propósito geral para simulação e modelagem de HMMs para a análise de seqüências biológicas
KDE - ambiente gráfico para estações Linux e Unix
MLE - Estimativa de máxima verossimilhança
HMM - Modelo markoviano oculto
L2 - Nível 2
LCS - Maior subseqüência comum
LL - log likelihood
Mb - Mega bytes
mb - Milhões de pares de bases
MEM - Memória
ML - Máxima verossimilhança
pb - Pares de bases
PC - Computador pessoal
PD - Programação dinâmica
Pfam - Banco de dados de famílias de proteinas, de alinhamentos e de HMMs (Protein families database of alignments and HMMs)
RAM - Memória de acesso aleatório (Randon Memory Access)
SAM - Sistema de modelagem e alinhamento de seqüências (Sequence Alignment and Modeling Systems)
CAPÍTULO 1
INTRODUÇÃO
1.1 Introdução
Esse trabalho trata de algoritmos de programação dinâmica que são usados em
Bioinformática (ou Biologia Computacional), mais precisamente nos Modelos
Markovianos Ocultos (HMMs) com topologias lineares, por exemplo nos perfis-HMMs.
O foco é a investigação de técnicas (métodos ou paradigmas) de economia de espaço,
que proporcionem a melhor economia de tempo e, que sejam adequadas para a
utilização no cálculo de medidas de interesse dos HMMs, mas que sejam, também,
adequadas no processo de treinamento destes modelos.
Nos organismos ou seres vivos são encontradas duas famílias de macromoléculas que
são especialmente estudadas, devido a sua importância no ciclo de vida destes
organismos ou seres: as proteínas e os ácidos nucléicos (ácidos ribonucléicos - RNA - e
ácidos desoxirribonucléicos - DNA). Essas macromoléculas são por conveniência
tratadas como seqüências biológicas, que são strings de um alfabeto finito de resíduos.
A estrutura primária das proteínas são vistas como seqüências (polímeros, cadeias)
compostas, geralmente, pelo encadeamento dos 20 aminoácidos. Os ácidos nucléicos
são vistos como seqüências compostas, geralmente, pelo encadeamento de 4
nucleotídeos.
A principal função das moléculas ou seqüências de DNA é armazenar e transmitir a
informação genética ou hereditária. Uma molécula de DNA contém vários genes, que
são as unidades funcionais e físicas básicas da hereditariedade. Cada gene é uma
seqüência específica de nucleotídeos que contém a informação necessária para a síntese
de uma determinada proteína. As moléculas de DNA estão organizadas em estruturas
que são denominadas cromossomos e o conjunto de todos os cromossomos de um ser
vivo constitui o seu genoma (ou genótipo), que em princípio codifica quase toda a
informação de sua biologia. Cada cromossomo é constituído por proteínas e DNA em
partes aproximadamente iguais. Estima-se que o genoma humano contenha 3 bilhões de
27
pares de bases (bp) agrupados em cerca de 30.000 a 40.000 genes e, que cada
cromossomo contenha entre 50 e 250 milhões de bp (mb). O genoma de outros
organismos vivos como o rato tem cerca de 3 bilhões de pares de bases e as bactérias
cerca de 600 mb.
As moléculas ou seqüências de RNA intervêm no processo de síntese das proteínas,
através do RNA mensageiro - RNAm – e do RNA transportador - RNAt. Já as proteínas
são elementos constituintes fundamentais das células e dos diversos tecidos dos seres
vivos, além de participarem de diversos mecanismos celulares como: catálise de reações
bioquímicas, transporte de outras moléculas, transmissão de mensagens, etc..
O estudo dos genes, da expressão gênica, da síntese protéica e das próprias proteínas
provê informações sobre o crescimento, a comunicação e a organização celular,
guiando-nos na elucidação e aprendizado do complexo funcionamento dos seres vivos.
Os biólogos estão interessados na função molecular que leva ao fenótipo – o conjunto
de características de um indivíduo, determinado pelo seu genótipo (ou genoma) e pelo
ambiente. Eles estão interessados no fluxo de informação entre o genótipo (ou genoma)
e o fenótipo. Esse processo principal que transforma a informação contida no DNA
genômico para os aspectos funcionais do organismo é conhecido em biologia como o
dogma central da genética molecular. Resumidamente diz-se que, com a intervenção de
determinados mecanismos celulares, do DNA fabrica-se RNA e deste RNA fabrica-se
proteína.
1) Existe um processo de desencadeamento, tratamento e manipulação da
informação contida na seqüência genômica antes que essa produza
concretamente um efeito biológico. São considerados cinco passos básicos no
processo de síntese protéica, nesse processo de transformação da informação
contida na seqüência genômica em função. Esses passos são:
2) Transcrição da informação contida na seqüência de DNA e síntese do(s)
fragmento(s) de pré-RNAm;
28
3) Splicing - Remoção dos Introns do pré-RNAm e junção dos diversos segmentos
Exons produzindo o RNAm. Esse processo está presente somente nos eucariotes
– organismos de mais alta ordem, como os humanos;
4) Tradução do RNAm em proteína;
5) Dobramento da proteína recém-sintetizada;
6) Função final da proteína produzida.
Essa manipulação da informação só é bem entendida para o processo de tradução. Para
os processos de transcrição e splicing existe um entendimento parcial do processo bio-
molecular que produz essa manipulação. Já para os processos de dobramento da
proteína e função final da proteína produzida existe um entendimento precário, que está
muito distante do ideal. Desta forma, dentre os vários desafios ou apostas da biologia
nos dias atuais é de interesse destacar os seguintes: a elucidação e compreensão da
informação genética, a cura através da modificação da informação genética, a
elucidação da expressão gênica e a compreensão e identificação das funções das
proteínas.
Esses desafios da biologia estão intimamente relacionados com diversos projetos do
genoma de espécies animais e vegetais. O objetivo primário dos Projetos do Genoma
Humano e de outros seres vivos é fazer uma série de mapas (diagramas descritivos) de
cada cromossomo com resoluções crescentemente mais finas. O processo de
mapeamento consiste de:
1) Dividir os cromossomos em fragmentos menores que possam ser multiplicados e
caracterizados (descritos);
2) Mapeamento - ordená-los para corresponder às suas localizações respectivas nos
cromossomos;
3) Determinar a seqüência de bases de cada um dos fragmentos de DNA
ordenados.
29
O objetivo final da pesquisa do genoma é encontrar todos os genes na seqüência de
DNA e desenvolver ferramentas para usar essa informação seja no estudo da biologia
humana e medicina, seja no estudo de animais e plantas de interesse agropecuário, seja
no interesse ecológico ou de preservação de espécies ameaçadas.
Para o sucesso destes projetos de elucidação do genoma de determinadas espécies é
necessário um investimento continuado em ferramentas analíticas e em banco de dados
novos e correntes. As ferramentas analíticas e os bancos de dados correntes precisam ser
constantemente adaptados para atender a evolução das necessidades da comunidade
científica. Além disso são necessárias novas ferramentas analíticas e bancos de dados
novos para as atividades que se encontram em expansão – expressão gênica e análise
funcional das proteínas, armazenamento e análise de dados de variação de uma dada
seqüência, e modelagem de redes biológicas complexas e de interações. Esses novos
métodos e tecnologias que estão revolucionando a pesquisa biológica, foram criados
para responder aos desafios explícitos ou gerados no seio destes projetos. Os métodos
experimentais diretos, que formam o ferramental clássico de pesquisa biológica, talvez
fossem capazes de resolver os problemas apresentados por esses projetos, mas a custos
impraticáveis e tempos inaceitavelmente longos, além dos problemas éticos que são
colocados pela sua utilização principalmente em seres humanos.
Desta forma, têm sido desenvolvidas novas técnicas de sequenciamento (sequencing) e
métodos de investigação por analogia para responder as desafios de redução de custo e
de tempo, e de superação do problema ético. Esses métodos permitem tirar os princípios
gerais a partir da comparação das seqüências entre si; por exemplo, determinar a função
de uma proteína a partir de sua estrutura química (primária) ou física (forma).
Entretanto esses métodos e técnicas são essencialmente modelos matemáticos que
necessitam ser implementados através de algoritmos que possam ser executados em
computadores, devido à sua natureza iterativa e/ou à grande quantidade de
processamento exigido.
Esses novos métodos e técnicas têm gerado uma quantidade crescente de informações
sobre seqüências biológicas - a produção de dados do genoma cresce diariamente -, o
30
que reforça a necessidade do uso de modelos matemáticos e de métodos computacionais
de análise para tratar essas informações. Mas impõe, também, a necessidade de
investigação desses e de novos modelos matemáticos capazes de analisar grande
quantidades de dados, bem como a investigação de técnicas computacionais que dêem
suporte a esses métodos.
Dentre os novos desafios que estão sendo visualizados pelos projetos do genoma
humano e de outros organismos vivos, que possivelmente serão o foco de explorações
biológicas nesse século e nos vindouros, se destaca o uso desse enorme estoque de
dados para explorar como o DNA, as proteínas e o ambiente se interagem para criarem
sistemas de vida dinâmicos e complexos. Essas explorações englobam os seguintes
estudos:
• Transcriptoma – refere-se ao genoma funcional ou genoma expresso. Análise em
grande escala de RNAm para determinar quando, onde e sob que condições
ocorrem a expressão dos genes;
• Proteoma – o estudo da função e da expressão das proteínas e a geração de
estruturas 3-D de uma ou mais proteínas de cada família de proteínas;
• Sequenciamento – uso de métodos laboratoriais e computacionais para a
aquisição da estrutura dita primária de uma dada seqüência biológica;
• Estudos knockout – são métodos experimentais que pretendem entender a função
de seqüências de DNA e descobrir quais as proteínas que elas codificam. Os
pesquisadores têm como estratégia a inativação de genes em organismos vivos e
o monitoramento de quaisquer mudanças que poderiam revelar a função de
genes específicos;
• Genoma comparativo – análise comparativa (side by side) de padrões de
seqüências de DNA humanos e de organismos bem estudados, denominados
modelos. Esse tipo de investigação tem revelado ser uma das mais poderosas
31
estratégias para a identificação de genes humanos e interpretação de suas
funções.
Uma nova disciplina ou campo de pesquisa tem se estruturado no seio da comunidade
científica internacional, para fazer face a esses desafios, como um esforço conjunto de
cientistas de várias áreas de pesquisa incluindo: Biologia, Computação, Matemática,
Tecnologia da Informação, Química, Física e Engenharia. Bioinformática ou Biologia
Computacional são os termos forjados para esse novo campo para gerenciar e analisar
os dados de seqüências biológicas, com os objetivos principais de modelagem e de
entendimento de sistemas viventes ou biológicos. Desta forma a Bioinformática ligou
duas ciências que vêm crescendo de forma exponencial nos últimos 20 anos, Biologia e
Computação.
Nesse ponto é importante salientar que existe um choque de culturas entre estes
cientistas com formação essencialmente numérica ou quantitativa (matemáticos, físicos,
engenheiros e cientistas da computação) e os biólogos. Os cientistas do campo numérico
ou quantitativo estão mais familiarizados com uma ciência com regras bem definidas e
com uma fundamentação matemática. Os biólogos tratam com uma ciência que tem
poucas regras e muitas exceções. Mas este choque tem provocado uma troca de
experiência positiva entre os dois campos de pesquisa.
Os termos Biologia Computacional e Bioinformática têm sido empregados, com
predominância do primeiro, a cada vez que se fala das mais recentes conquistas feitas
nas áreas relacionadas ao genoma, ao proteoma e à expressão de proteínas. Embora
nunca tenha existido uma definição única e precisa para os termos Biologia
Computacional e Bioinformática, esses termos têm sido utilizados para se referir às
questões científicas e às questões de tecnologia e infra-estrutura computacional,
respectivamente. Biologia Computacional tem se referido a atividades concentradas na
construção de algoritmos para tratar problemas com relevância biológica, ao estudo de
sistemas moleculares naturais e artificiais, bem como aos novos paradigmas de
computação baseada em DNA. Bioinformática tradicionalmente tem se referido às
atividades concentradas na construção e no uso de ferramentas computacionais para a
32
análise de dados biológicos disponíveis, geralmente provenientes dos diversos projetos
de genomas, a fim de elucidar processos biológicos complexos.
Entretanto, é importante salientar que essa distinção serve principalmente para realçar a
área de concentração principal do estudo e, que, na prática, os dois termos são usados
indistintamente. Mas o que mais se popularizou foi Bioinformática, que hoje é
empregado indistintamente para ambas as designações, pois ele tem sido usado para
designar tanto o que tradicionalmente se conhece por Bioinformática quanto o que
historicamente era conhecido como Biologia Computacional. O uso do termo
Bioinformática, no seu sentido mais amplo, parece cada vez mais convergir para a
designação de uma agregação de tecnologias.
De um ponto de vista computacional as atividades inerentes à Bioinformática vão desde
a construção de ferramentas para problemas biológicos específicos, utilizando teoria
algorítmica, ao trabalho experimental, onde um laboratório com tubos de testes e
microscópios são substituídos por um computador, com grande capacidade
computacional e de armazenamento, e um disco rígido cheio de ferramentas
computacionais projetadas para analisar grandes quantidades de dados biológicos para
provar ou descartar uma dada hipótese. Dentre os desafios que a Bioinformática tenta
dar respostas, atualmente, podem ser arrolados os seguintes questões ou problemas:
1) Filogenia – Trata-se da construção de árvores filogenéticas baseadas em
observações nos organismos existentes para modelar a evolução das espécies. Os
organismos que têm sido estudados até o momento apresentam similaridades
suficientemente fortes nos mecanismos moleculares, que induz a suposição da
existência de um ancestral comum para todos os organismos na Terra. Duas
seqüências que compartilham um ancestral comum são ditas homólogas. Essas
árvores podem servir como guias na comparação (alinhamento) de seqüências,
permitindo medidas de similaridades biologicamente mais significativas.
2) Sequenciamento (sequencing) – Trata do uso de técnicas laboratoriais e de
técnicas computacionais para a aquisição da estrutura dita primária de uma dada
seqüência, geralmente de DNA.
33
3) Determinação da função e da estrutura tridimensional da proteína – Trata-se do
uso de métodos e técnicas computacionais para determinar a função e a estrutura
tridimensional das proteínas. Até o momento, não é possível deduzir a forma
tridimensional de uma dada proteína somente a partir de sua cadeia de
aminoácidos. A estrutura tridimensional fornece muito mais informações sobre a
função da proteína que o simples encadeamento linear de aminoácidos (estrutura
primária das proteínas). Parece ser necessário levar em conta tanto a estrutura
secundária, terciária e quaternária da proteína, quanto a sua filogenia.
4) Comparação e alinhamento de seqüências – Trata-se de técnicas e métodos de
análise de seqüências por analogia, em que os princípios gerais são tirados a
partir da comparação das seqüências entre si. Estes métodos, que são baseados
nos conceitos de homologia e similaridade, permitem:
• Estimar a diferença ou similaridade entre duas seqüências;
• Construir um alinhamento para mostrar graficamente as diferenças e
similaridades das seqüências, arranjando lado-a-lado em filas os
elementos concordantes, os discordantes e as lacunas;
• Verificar o grau de similaridade de uma dada seqüência a um certo
conjunto de seqüências;
• Criar um modelo representativo de um dado conjunto de seqüências
(família, domínio, etc.);
• Determinar a função de uma proteína (ou de uma outra dada estrutura, ou
de uma família de proteínas) a partir da similaridade de suas estruturas
químicas e/ou físicas;
• Buscar num banco de dados por seqüências pertencentes a uma dada
família ou domínio, ou buscar por seqüências que sejam similares a um
dado conjunto de seqüências.
34
Os organismos vivos evoluem através de gerações por mutação e seleção natural.
Dizemos que duas estruturas (proteínas ou ácidos nucléicos), aqui representadas por sua
seqüência de resíduos, são homólogas se elas descendem de um ancestral comum. Desta
forma a evolução de seqüências biológicas acumula adições, remoções e substituições
de resíduos. Então a avaliação da similaridade entre duas seqüências biológicas inicia-se
pela determinação de um alinhamento plausível entre elas e passa pela avaliação de sua
significância (ou relevância) biológica.
A abordagem de programação dinâmica (PD) é atualmente o método mais popular para
comparação e alinhamento rigoroso de seqüências de ácidos nucléicos e proteínas. Esse
método fornece o resultado da comparação e encontra um alinhamento entre duas
seqüências com o mais alto escore possível para vários esquemas de atribuição de
escores úteis, incluindo funções afins de atribuição de escores com lacunas (affine gap
scores). Praticamente todos os métodos de comparação de seqüências biológicas
encontram o melhor alinhamento entre duas strings sob um determinado esquema de
atribuição de escores. A relevância (ou significância) biológica de um alinhamento está
intimamente relacionada ao esquema de atribuição de escores adotado. Além da
utilização da PD em métodos de comparação e/ou alinhamento de seqüências
biológicas, a abordagem da PD tem sido utilizada em um grande número de outros
problemas computacionais em Bioinformática.
A abordagem de PD constitui o corpo principal de praticamente todos os algoritmos e
métodos utilizados para a comparação de seqüências biológicas. Os algoritmos que
empregam a abordagem de PD são usados em Bioinformática para alinhamento de pares
de seqüências ou de múltiplas seqüências entre si (perfil), para alinhamento de uma ou
múltiplas seqüências a um perfil-HMM ou para treinamento de um perfil-HMM. Esses
algoritmos podem ser utilizados, com diversos critérios de otimização (funções de
custo), para encontrar um alinhamento ótimo (de custo mínimo ou de score máximo) ou
calcular outras medidas de interesse. Em biologia molecular as funções de custo típicas
incluem, em ordem crescente de complexidade:
1) custos simples, onde cada custo de mutação é uma constante;
35
2) custos lineares de lacunas (gaps) e custos lineares de lacunas afins, onde
inclusões e remoções são orçadas através de uma função linear;
3) custos lineares por partes de lacunas e custos côncavos de lacunas.
A qualidade de um algoritmo é, tradicionalmente, medida através de metodologia
algorítmica padrão em termos de recursos que esse requer para resolver o problema,
principalmente tempo e espaço. Entretanto, tratando-se de algoritmos para a resolução
de problemas com relevância biológica, a qualidade destes deve ser avaliada através da
combinação de seu tempo de execução, de seu requerimento de espaço e da relevância
biológica das respostas que esse produz. Desta forma a construção de bons algoritmos
em Bioinformática envolve um intercâmbio entre a modelagem biológica e a construção
do algoritmo até que um balanço razoável entre os requerimentos de espaço, de tempo e
de relevância biológica seja alcançado.
Os HMMs são modelos estocásticos sofisticados e flexíveis utilizados como uma
ferramenta para o estudo e análise de sinais. Esses modelos muitas vezes são referidos
na literatura como fontes de informações Markovianas (sources Markov) ou como
funções probabilísticas de cadeias de Markov. Os HMMs fornecem uma estrutura geral
para a análise estatística de uma grande variedade de modelos de sinais. Dentre as
diversas aplicações dos HMMs destacam-se: reconhecimento ou processamento de fala,
veja, por exemplo, Rabiner (1989); processamento de sinais; processamento de imagens
e visão por computador (computer vision); reconhecimento de texto e manuscritos;
acústica (reconhecimento de ruídos e sons), veja, por exemplo, Couvreur et al. (1998);
climatologia (padrões atmosféricos e precipitações); controle; comunicações;
reconhecimento de sinais de ECG (eletrocardiograma), veja, por exemplo, Koski
(1996); discriminação entre surtos de corrente e falha interna de corrente em
transformadores de potência, veja, por exemplo, Ma e Shi (2000); biologia
computacional – comparação, reconhecimento, discriminação e busca na modelagem de
seqüências nucléicas (DNAs ou RNAs) e proteínas (genoma), veja, por exemplo,
Hughey e Krogh (1996), Kehagias (1996), Karplus et al. (1998), Koski (1999) e Cuesta
36
et al. (2003). Para uma boa lista bibliográfica abrangendo essas diversas aplicações veja
Cappé (2001).
Os HMMs têm um grande potencial para modelagem de seqüências de nucleotídeos ou
de aminoácidos em biologia molecular. A idéia básica é construir um modelo que
descreva um conjunto de seqüências e, então, usá-lo para comparar uma seqüência
contra o modelo, para encontrar seqüências relacionadas, ou para examinar o modelo e
determinar propriedades das seqüências. Ou seja, responder a questões como ‘Quais
seqüências num banco de dados se ajustam a um modelo?’ ou ‘Quão bem elas
ajustam?’ ou ‘Qual de um conjunto de modelos melhor descreve um conjunto de
seqüências?' (Durbin et al., 1998).
A modelagem utilizando os HMMs apresenta vantagens significativas:
• Proporcionam a construção de um modelo probabilístico explícito para um
determinado conjunto de seqüências biológicas.
• Os parâmetros são específicos para cada posição no modelo, enquanto outros
modelos de comparação de seqüências usam estatísticas gerais para a
probabilidade de ocorrência dos símbolos.
Entretanto a modelagem utilizando os HMMs apresenta algumas desvantagens:
• O número de parâmetros do modelo depende do tamanho das seqüências
pertencentes ao conjunto que está sendo modelado. Como as seqüências
biológicas freqüentemente chegam a dezenas de milhares de resíduos, o número
de parâmetros do HMM pode tornar-se suficientemente grande para que o
processo de estimação destes parâmetros se torne impraticável na maioria dos
sistemas computacionais uni-processados (PCs, WorkStations) que se encontram
ativos nos laboratórios de pesquisa.
• A modelagem de um problema complexo (difícil) de otimização usando HMMs,
freqüêntemente, produz um modelo muito complexo.
37
Os algoritmos de PD são utilizados na solução dos três problemas básicos na
modelagem usando HMMs:
• Determinação da probabilidade de ocorrência de uma certa seqüência de
observações O dado o modelo λ, P(O/λ);
• Determinação da seqüência de estados Q que melhor elucida a seqüência de
observações O;
• Treinamento do modelo, que nada mais é do que o processo iterativo de
determinação dos parâmetros do modelo para que a P(O/λ) seja maximizada.
Esse trabalho foi organizado da seguinte forma. Nas demais seções deste capítulo são
apresentadas: a formalização do problema, os objetivos e hipóteses e uma justificativa
deste estudo. No capítulo 2 são apresentados as definições e os conceitos básicos da
teoria dos HMMs, dos HMMs lineares – mais precisamente os perfis-HMMs que têm
sido usados na análise de seqüências biológicas –, da teoria de análise de complexidade
de algoritmos e dos algoritmos de programação dinâmica. No capítulo 3 é feita uma
revisão da literatura sobre algoritmos de PD com economia de espaço, seguida de uma
conceituação básica dos algoritmos de PD que fazem uso do paradigma de checkpoints.
No capítulo 4 apresenta-se os algoritmos alternativos propostos nesse trabalho. No
capítulo 5 apresenta-se a metodologia de experimentação, os resultados da
experimentação e faz-se a análise destes resultados. No capítulo 6 apresenta-se as
conclusões e comentários.
1.2 O Problema
O sujeito deste trabalho é constituído dos HMMs (lineares ou perfis) utilizados no
estudo de seqüências biológicas no campo da Bioinformática. O objeto deste trabalho é
constituído do estudo de algoritmos de programação dinâmica, sob a ótica da
complexidade de espaço e de tempo, que possam ser utilizados nos HMMs para o
cálculo de P(O/λ), para decifrar (ou revelar) o caminho de estados Q que melhor elucida
38
a produção da seqüência de observações O pelo modelo λ e para o treinamento do
modelo λ.
A complexidade de espaço e de tempo dos algoritmos de PD são, respectivamente, de
ordem O(n2) bytes e O(n3) operações, considerando-se que as strings e o HMM são de
comprimentos similares, digamos ~n, ou tomando-se o comprimento maior. Com
relação à complexidade de tempo, uma outra interpretação usada por Grice et al. (1997),
Tarnas e Hughey (1998) e Wheeler e Hughey (2000), considera um tempo unitário para
efetuar os cálculos de uma célula de PD. Sob essa ótica a complexidade de tempo seria
de ordem O(n2) unidades de tempo. Nesse trabalho foi adotada essa ótica alternativa
para expressar a complexidade de tempo, principalmente para facilitar as comparações
com os resultados obtidos em Tarnas e Hughey (1998).
Uma seqüência biológica típica pode conter de centenas a dezenas de milhares de
símbolos de um dado alfabeto – com pelo menos 4 símbolos para seqüências de DNAs
(ou RNAs) e 20 símbolos para seqüências de aminoácidos. Os algoritmos padrões de
programação dinâmica utilizados para computar medidas de interesse, requerem muito
espaço de armazenamento (memória de trabalho ou RAM) e gastam muito tempo de
execução (tempo de CPU e/ou wall time), limitando, portanto, o comprimento máximo
de seqüências biológicas que podem ser analisadas nas máquinas seriais
comercializadas atualmente (PCs e Workstations).
O espaço requerido para armazenar a matriz completa de PD pode exceder a capacidade
física da máquina, mesmo desconsiderando os requerimentos de espaço para o Sistema
Operacional e o próprio programa que executa o algoritmo de PD. Desta forma, o tempo
de execução pode ser agravado pela ineficiência no uso do sistema de memória. A
execução destes algoritmos de PD em máquinas seriais pode até mesmo se tornar
impraticável para seqüências não muito longas (na casa de dezenas de milhares).
Existem técnicas usualmente referidas como métodos de espaço linear, que alinham um
par de seqüências, ou uma seqüência a um HMM, usando memória limitada. Alguns
destes algoritmos fazem uso do princípio dividir e conquistar (D&C) e requerem espaço
39
da ordem de ( nmO )+ ao invés de ( )nmO × , ao preço de dobrar o tempo de execução
(Hirschberg, 1975). Há outros, que são denominados algoritmos com -níveis de
checkpoints e fazem uso de uma técnica que armazena linhas, colunas ou diagonais da
matriz de PD (checkpoints) para que a computação de seções da matriz de PD possa ser
refeita sem a necessidade de re-iniciar a computação desde a primeira iteração, que, em
geral, requerem espaço da ordem de
L
( )m×nLO , ao preço de aumentar o tempo de
execução por um fator de para alinhar um par de seqüências (Grice et al., 1997),
(Tarnas e Hughey, 1998) e (Wheeler e Hughey, 2000). Existem outros, como o
algoritmo de Powell, que recorrem a esses dois princípios (Powell et al., 1999).
L
No processo de treinamento de HMMs lineares (ou perfis) é necessário recuperar toda a
matriz de PD, o que descarta o uso de técnicas ou estratégias tradicionais que empregam
o princípio D&C, bem como o uso da estratégia adotada em Powell et al. (1999) para a
redução do espaço de armazenamento nos algoritmos de PD.
Os algoritmos que adotam a técnica ou estratégia de checkpoints possibilitam economia
de espaço (memória) e podem ser usados para a redução da complexidade de espaço nos
HMMs lineares (ou perfis). Apesar do potencial de uso destes algoritmos em HMMs
lineares (ou perfis), esses não têm sido, ainda, completamente investigados.
1.3 Objetivos e Hipóteses
Esse trabalho tem o propósito de fazer um estudo dos algoritmos de PD que recorrem à
estratégia de checkpoints como técnica de economia de espaço, que formam o núcleo do
treinamento e da computação das medidas de interesse nos HMMs e, que são utilizados
em máquinas seriais. Pretende-se analisar as soluções para o problema da complexidade
de espaço levando-se em consideração o impacto destas soluções no desempenho destes
algoritmos.
Tem-se como objetivos a proposição de algoritmos novos através da exploração de
alterações nesses algoritmos de PD com checkpoints que iguale ou reduza os
requerimentos de espaço e melhore o desempenho em relação ao estado da arte destes
algoritmos, ou pelo menos não provoque aumentos consideráveis nos requerimentos de
40
tempo de execução destes algoritmos. Mais precisamente, a proposição de algoritmos
novos baseados na exploração da hipótese de utilização da estratégia de checkpoints em
conjunto com o princípio D&C como solução consistente para o problema de
complexidade de espaço versus complexidade de tempo dos algoritmos de PD utilizados
no cálculo de medidas de interesse em HMMs lineares (ou perfis).
Essa hipótese da utilização da estratégia (ou paradigma) de checkpoints em conjunto
com o princípio de D&C nos algoritmos de PD já foi explorada, ainda que de forma não
explícita, utilizando-se checkpoints por diagonais com retrocedimento restrito. Mas
parece ser mais promissora utilizando-se explicitamente o principio D&C em conjunto
com o paradigma de checkpoints em duas dimensões da matriz de PD, por linhas e por
colunas, simultaneamente.
1.4 Justificativa
Os melhoramentos introduzidos no algoritmo de PD básico proporcionam
requerimentos de memória da ordem de O(n) ou ( )nn ×O , que em termos práticos
apresentam pouca ou nenhuma queda no desempenho quando comparados ao algoritmo
de PD padrão. Apesar destes melhoramentos, os requerimentos de espaço em conjunto
com os requerimentos de tempo continuam sendo um gargalo na avaliação de
seqüências de aminoácidos ou de nucleotídeos em biologia molecular, pois as
seqüências, freqüentemente, podem atingir a casa dos milhões e a memória disponível é
sempre limitada.
É bom lembrar que os recursos de memória são sempre limitados e constituem um
gargalo nos sistemas computacionais. O desenvolvimento tecnológico e a conseqüente
redução de custos e aumento da capacidade dos recursos computacionais, possibilitam a
superação de limitações com relação a solução computacional de instâncias de
determinados problemas, mas, por outro lado, possibilitam o tratamento computacional
de outros problemas, novas fronteiras, que estavam fora do alcance do poder
computacional imediatamente anterior, possivelmente tornando esses novos recursos
computacionais novamente limitados. Adicionalmente, deve ser considerado, também,
41
que nas máquinas atuais o acesso: à memória cache requer 1 ciclo de clock, à memória
principal de 10 a 20 ciclos, e ao disco rígido cerca de um milhão de ciclos (Tarnas e
Hughey, 1998). Desta forma execuções com requerimentos de memória maiores do que
o tamanho da memória principal, podem tornar o custo de paginação da memória virtual
muito alto, tornando a computação, efetivamente, inviável.
Esses fatos por si só já são bastante significativos para realçar a necessidade de
investimentos em pesquisas para melhorar o desempenho destes algoritmos ou buscar
outros algoritmos alternativos. Desta forma, o estudo se concentra, a princípio, nos
algoritmos que adotam a estratégia de checkpoints com requerimentos reduzidos de
memória.
Os algoritmos alternativos baseados na estratégia D&C e no algoritmo rápido de
Ukkonen apresentados em Powell et al. (1999), que requerem espaço linear sem causar
queda de desempenho nos algoritmos de PD, podem ser acelerados aumentando os
recursos de memória. Entretanto eles somente são apropriados para determinar melhor o
alinhamento entre seqüências, que requer apenas o retrocedimento parcial sobre a matriz
de PD, mas não são apropriados para utilização no treinamento de HMMs ou qualquer
uso em conjunto com o algoritmo forward-backward requerendo todos os caminhos,
que exigem procedimentos de retrocedimento completo sobre a matriz de PD. Isto acaba
se tornando uma grande desvantagem quando o interesse reside justamente em
desenvolver um pacote de ferramentas de modelagem de HMMs, pois não poderíamos
ter um algoritmo base que fosse o núcleo de todo o processamento de PD.
Deve-se considerar, também, que as funções de custo freqüentemente usadas nos
HMMs em biologia molecular não geram matrizes de PD com diagonais não-
decrescentes, pois essas funções usam atribuição de escores em log-probabilidades (log-
odds scoring), impedindo a utilização de algoritmos que exploram essa característica
para acelerar o desempenho computacional. Muitas vezes, ao invés de se usar os escores
em log-probabilidades (log-odds score) relativos ao modelo aleatório, o logaritmo da
probabilidade da seqüência dado o modelo é usado diretamente, que é denominado
42
escore LL (de log likelihood score), entretanto o LL score é fortemente dependente do
tamanho das seqüências, apresentando uma dependência não linear.
O método de PD com checkpoints apresenta alguns méritos: a sua forte independência
do algoritmo subjacente; a redução do requerimento de espaço da ordem de O para (nm)
( )L nmO ; e a possibilidade de se aumentar o desempenho em troca de um aumento na
constante de espaço. Além disso eles são a única alternativa de economia de espaço
apropriada para o treinamento de HMMs ou para qualquer outro uso em conjunto com o
algoritmo forward-backward requerendo todos os caminhos.
Dessa família de algoritmos de PD com checkpoints, aquele por diagonais com o
processo de retrocedimento restrito, que alia a técnica (ou paradigma) de checkpoints
com o princípio D&C, é o que, segundo Tarnas e Hughey (1998), apresentou melhor
desempenho no alinhamento de seqüências. Entretanto foi observado, com base nos
experimentos realizados e na experiência anterior com esses algoritmos, que a
complexidade das condições de contorno, adicionadas quando se processa diagonais,
não proporcionou um retorno equivalente no ganho de performance. Desta forma, com
base nos resultados obtidos eles planejaram reimplementar o loop interno do pacote
SAM (Hughey, 2003) com checkpoints por linhas. É importante salientar que a
complexidade das condições de contorno foi agravada pelo uso da técnica de
retrocedimento restrito. Com base nestas conclusões o grupo de pesquisadores
responsáveis pelo pacote SAM buscou alternativas para melhorar o desempenho destes
algoritmos com checkpoints por linhas.
Wheeler e Hughey (2000) apresentaram um algoritmo melhorado de (1,2)-níveis de
checkpoints por linhas que, assintoticamente, efetua até 50% menos cálculos quando
comparado ao original com checkpoints por linhas. Entretanto na prática os resultados
mostraram que o algoritmo melhorado, na avaliação de todos os caminhos, é 3 a 12%
mais rápido que o de 2-níveis original, independente do tamanho do HMM. É
importante lembrar que o algoritmo de PD de 2-níveis original, com checkpoints por
linhas, tem requerimentos de memória da ordem de ( )nmO , ao custo de uma queda de
desempenho por um fator de 2 com relação ao algoritmo de PD padrão. Nesse ponto
43
deve ser salientado que esses melhoramentos relatados por Wheeler e Hughey (2000)
podem ser utilizados, quando for conveniente, juntamente com outros melhoramentos
nos diversos algoritmos membros desta família de algoritmos com checkpoints.
Deve-se salientar também que, talvez, o mais importante a ser considerado nessas
técnicas de uso de espaço de armazenamento restrito seja o comportamento assintótico
da redução dos requerimentos de memória, pois elas são, a princípio, úteis justamente
para seqüências longas, ou seja, quando os requerimentos de memória crescem com o
tamanho destas seqüências. Desta forma, a redução dos requerimentos de memória da
ordem de O(mn) para ( )nmO e para O(m) possuem um comportamentos assintóticos
bastante semelhante, para m e n suficientemente grandes.
Considerando-se, portanto, o tradeoff entre a redução dos requerimentos de espaço e a
performance dos algoritmos de PD de L-níveis com checkpoints e o comportamento
assintótico com relação à complexidade de tempo, da ordem de O(mn), do algoritmo de
PD de 2-níveis com checkpoints por colunas ou linhas, quando determinando um
alinhamento entre duas seqüências ou calculando o melhor caminho simples de estados
através de um HMM, verifica-se que ainda há necessidade de investimento em pesquisa
para explorar alternativas que melhorem o desempenho destes algoritmos.
Portanto, ainda existe necessidade de se explorar alterações nessa família de algoritmos
de PD que usam checkpoints e reavaliação, que tiram vantagem do uso de espaço linear
através da armazenagem de checkpoints, mas ao custo de uma queda no desempenho,
devido à reavaliação, quando comparados com o algoritmo PD tradicional. É necessário
explorar novos algoritmos que mantenham a complexidade de espaço linear e cause um
impacto menor na queda de desempenho.
Esse trabalho contribui para o campo de Bioinformática investigando e propondo novos
algoritmos alternativos que utilizam a estratégia (ou paradigma) de checkpoints em
conjunto com o princípio D&C como solução para o problema de complexidade de
espaço dos algoritmos de PD usados nos HMMs, levando-se em consideração o
44
aumento dos requerimentos de tempo para execução, provocado pela necessidade de
reavaliação de partes da matriz de PD.
Até onde se tem conhecimento não existe nenhuma investigação relatada na literatura
pertinente, que trate da hipótese levantada nesse trabalho.
45
46
CAPÍTULO 2
CONCEITOS BÁSICOS
2.1 Introdução
Nesse capítulo é feita uma revisão da teoria básica de complexidade de algoritmos e da
classe de HMMs. Essa revisão é focada principalmente nos tópicos de interesse deste
trabalho, deixando de abordar aqueles que não são de interesse direto para a pesquisa
em foco. Na segunda seção são apresentados os conceitos fundamentais dos HMMs e as
definições de interesse para a Bioinformática. Na terceira seção são apresentados alguns
conceitos fundamentais de complexidade de algoritmos. Na quarta seção são
apresentados conceitos e descrições dos algoritmos de programação dinâmica.
2.2 Modelos Markovianos Ocultos (HMMs)
Os conceitos e definições apresentados nessa seção estão fundamentados principalmente
nos tutoriais de Rabiner (1989) e de Grate et al. (1999), no manual de Durbin et al.
(1998), no curso de Koski (1999), nos manuais de usuário dos pacotes HMMER (Eddy,
2003) e SAM (Hughey et al, 2003) e nos artigos de Krogh et al (1993), de Haussler et al
(1993), de Hughey e Krogh (1996), de Barret et al. (1997), de Eddy (1998), de Birney
(2001) e de Sato et al. (2003). Na seção 2.2.1 é apresentada uma definição formal para
os HMMs. Na seção 2.2.2 são abordados os três problemas fundamentais dos HMMs e
as soluções apresentadas para esses problemas. Na seção 2.2.3 é esboçada a classe de
HMMs lineares que têm sido estudados em biologia molecular
2.2.1 Definições
Os HMMs estendem o conceito de modelos markovianos para incluir o caso onde a
observação (situação observável) é uma função probabilística do estado. Um HMM
refere-se a um processo estocástico cujas transições entre estados obedecem a uma
cadeia de Markov não observável (daí o nome escondido ou oculto) e que a cada estado
visitado emite (ou produz) um símbolo observável (uma observação) conforme uma
distribuição de probabilidades (estado discreto) ou uma função densidade (estado
47
contínuo). Os HMMs consistem de cinco blocos (ou elementos) básicos que são
definidos formalmente, como:
1) Um conjunto finito de N estados, designado por S = S1, S2, ..., SN, no qual
define-se um processo estocástico qt que indica o estado do modelo no instante t
. ( )L,3,2,1,0=t
2) Um alfabeto discreto e finito correspondente a K símbolos distintos de
observações (ou de emissões), designado por V = v1, v2, ..., vK. Os símbolos
de observações correspondem à saída física do sistema que está sendo modelado.
3) Uma distribuição de probabilidades de transição de estados denotada por A =
aij, onde aij = P [qt+1 = Sj | qt = Si], 1 i, j N.
4) Uma distribuição de probabilidade de observação (ou emissão) do símbolo vk
quando o processo atinge o estado j, designada por E = ej(k) , onde ej(k)
= P [ Ot = vk | qt = Si ], 1 j N e 1 k K.
5) Uma distribuição de probabilidade para o estado inicial π = πi , onde πi
= P [q1 = Si] e 1 ≤i ≤ N.
Desta forma uma especificação completa de um HMM requer a especificação dos
parâmetros N e K, a especificação dos símbolos de observação (ou emissão) e a
especificação das três distribuições de probabilidade: A, E e π. Mas, por conveniência
usa-se uma notação compacta designada por λ = (A, E, π) para indicar o conjunto de
parâmetros do modelo. Um exemplo de um HMM discreto com 3 estados e 5 símbolos
de observação é mostrado na Figura 2.1.
48
FIGURA 2.1 - HMM discreto com três estados e 5 símbolos de observação.
2.2.2 Os Três Problemas Básicos dos HMMs
Segundo Rabiner (1989), para que os HMMs sejam úteis em aplicações do mundo real é
necessário apresentar soluções para os três problemas básicos de interesse formulados a
seguir.
“Problema 1 : Dada uma seqüência de observações O = O1 O2 … OT, e um modelo λ =
(A, E, π), como podemos computar eficientemente P(O|λ), isto é, a probabilidade de
ocorrência da seqüência de observações O, dado o modelo λ?“ .
“Problema 2 : Dado uma seqüência de observações O = O1 O2 … OT, e um modelo λ =
(A, E, π), como podemos escolher uma seqüência de estados Q = q1 q2 … qT que seja
ótima segundo algum critério significativo, isto é, que melhor elucida as observações?”.
“Problema 3 : Como podemos ajustar os parâmetros do modelo λ = (A, E, π) para que
seja maximizada a P(O|λ)?“.
2.2.2.1 Soluções para o Problema 1
Uma dada seqüência de observações O pode ser gerada por muitos caminhos diferentes
através dos estados de um HMM; ou seja, por muitas seqüências distintas e possíveis de
estados S do modelo λ. Portanto a probabilidade de ocorrência de uma certa seqüência
de observações O é a soma das probabilidades sobre todos os caminhos distintos e
possíveis de estados através do modelo λ, ou seja:
49
( ) ( ) ( ) ( )
( ) ( )∏∑
∑∑
=−
=
===
T
ttqqq
allQqq
allQallQ
OeaOe
QPQOPQOPOP
ttt2
1 111
|,||,|
π
λλλλ
(2.1)
Para se fazer uma análise de pior caso do esforço computacional requerido para
computar a equação (2.1), é considerado um HMM onde qualquer estado pode ser
alcançado dele mesmo e de qualquer outro estado. Pode ser observado com certa
facilidade, que são requeridas (2T -1)N T multiplicações e N T -1 adições, sendo,
portanto, requeridas (2T -1)N T + N T -1 = (2T)N T -1 operações. Isto significa que o
número de operações cresce exponencialmente quando o tamanho da seqüência cresce,
de onde se conclui que o cálculo direto de P(O|λ) através da equação (2.1) é
computacionalmente inviável, exceto para valores muito pequenos de N e T.
No entanto, o cálculo de P(O|λ) definido na equação (2.1) pode ser realizado de forma
muito mais rápida usando um algoritmo de programação dinâmica como o forward ou o
backward, que requer a realização de 2 operações.
Desde que T , pode-se considerar sem perda de generalidade que são requeridas
( 122 222 −≤++− TNNNTNTN )N≥3223 22 TTT ≤− operações, ou seja, uma complexidade de tempo dominada por uma
equação polinomial de grau 3.
Nesse sentido, define-se a variável forward ft(i) como a probabilidade de um certo
prefixo até um determinado estado, dado o modelo λ, ou seja, ft(i) = P(O1…Ot, qt=Si|λ).
A variável forward ft(i) pode ser resolvida recursivamente da seguinte forma:
algoritmo forward
1) Inicialização: f ( ) ( ) NiOei ii ≤≤= 1 ,11 π .
2) Recursão: f . ( ) ( ) ( ) NjT-t aifOejN
iijttjt ≤≤≤≤= ∑
=++ 1 ,11 ,
111
3) Encerramento: P . ( ) (∑=
=N
iT ifO
1| λ )
50
Como tanto o algoritmo forward como o algoritmo backward serão requeridos na
solução dos problemas 2 e 3, define-se, a seguir, o algoritmo backward que
alternativamente pode ser utilizado para a avaliação de P(O|λ). Nesse sentido, seja a
variável backward bt(i) definida como a probabilidade de um certo sufixo até um
determinado estado, dado o modelo λ, ou seja bt(i) = P(Ot+1 Ot+2 … OT|qt=Si, λ). A
variável backward bt(i) pode ser resolvida recursivamente da seguinte forma:
algoritmo backward
1) Inicialização: b ( ) NiiT ≤≤= 1 ,1 .
2) Recursão: b ( ) ( ) ( ) NiT-T- tjbOeaiN
jttjijt ≤≤== ∑
=++ 1 ,1,,2,1 ,
111 K
3) Encerramento: P ( ) ( ) (∑=
=N
iii OeibO
111| πλ )
)
2.2.2.2 Soluções para o Problema 2
Se a seqüência de estados Q, que melhor elucida uma determinada seqüência de
observações O, é conhecida, então a probabilidade conjunta de O e Q pode ser
facilmente computada, dado o modelo λ, simplesmente avaliando a seguinte expressão.
( ) ( ) (∏=
−=
T
ttqqqqq OeaOeQOP
ttt2
1 111|, πλ (2.2)
Entretanto, o caminho de estados Q freqüentemente não é conhecido e, desta forma, a
equação (2.2) não pode ser utilizada.
Embora não seja possível dizer em qual estado o sistema modelado por um HMM
esteja, simplesmente olhando para o símbolo observado, muitas vezes o interesse reside
justamente na seqüência de estados subjacentes que melhor elucida uma dada seqüência
de observações.
51
O problema de encontrar uma seqüência de estados ‘ótima’ associada com uma dada
seqüência de observações é freqüentemente referido na literatura como decifragem ou
decodificação. Existem várias vias possíveis para resolver o problema de decifragem, a
dificuldade reside justamente no significado de seqüência de estados ótima, pois
existem vários critérios de otimalidade.
Na verdade, em situações práticas, normalmente, recorre-se a um critério de otimalidade
para resolver esse problema de uma maneira mais adequada à aplicação, pois existem
vários critérios de otimalidade razoáveis que podem ser impostos. Desta forma, a
escolha de um critério é fortemente influenciada pela aplicação, ou seja, pelo uso
pretendido para a seqüência de estados em questão.
Embora outros critérios de otimalidade sejam razoáveis para algumas aplicações, os
dois critérios de otimalidade mais populares na decifragem de HMMs são:
1) maximizar o número esperado de estados individuais ‘ótimos’, escolhendo o
mais provável estado a cada época t;
2) encontrar a melhor seqüência, simples, existente de estados, ou seja, encontrar o
caminho mais provável.
Para implementar a solução baseada no primeiro critério, o de maximizar o número
esperado de estados individuais ótimos, define-se a variável γt(i) como a probabilidade a
posteriori do estado Si no instante t, dado a seqüência de observações O e o modelo λ:
( ) ( )λγ ,| OSqPi itt == (2.3)
A equação (2.3) pode ser expressa em termos das variáveis forward-backward, ou seja,
por ft(i) = P(O1 O2 … Ot, qt=Si|λ) e bt(i) = P(Ot+1 Ot+2 … OT|qt = Si, λ).
( ) ( ) ( )( )
( ) ( )( ) ( )∑
=
== N
jtt
ttttt
jbjf
ibifOP
ibifi
1
| λγ
(2.4)
52
Pode ser observado de (2.4) que o denominador P(O|λ) é um fator de normalização, que
faz com que γt(i) seja considerada uma medida de probabilidade, ou seja, ∑ . ( ) 11
=−
N
it iγ
Desta forma, γt(i) pode ser usado para avaliar individualmente o estado qt mais provável
a cada instante t, através da equação
( )[ ] Ttiq tNi
t ≤≤=≤≤
1 ,maxarg1
γ . (2.5)
Podem existir problemas com a seqüência de estados resultante da avaliação da equação
(2.5), pois essa é determinada a cada instante sem considerar a probabilidade de
ocorrência da seqüência de estados, ou seja, se o HMM em consideração tem
probabilidade de transição aij = 0 para algum i e j, então a seqüência de estados ‘ótima’
pode não ser uma seqüência de estados válida.
Segundo Rabiner (1989), uma segunda, e talvez mais importante, abordagem de
decifragem baseada nesse critério surge quando estamos interessados em alguma
propriedade derivada da seqüência de estados e não na própria seqüência de estado
‘ótima’. Pode-se estar interessado na probabilidade a posteriori de um certo símbolo vk
vir de um determinado subconjunto de estados. Assumindo-se uma função g(Si) definida
nos estados S, pode-se obter as probabilidades à posteriori de um certo símbolo vk vir de
um determinado subconjunto destes estados como
( ) ( ) (∑=
==N
iiitk SgOSqPOvG
1,|| λ ) . (2.6)
Um caso especial, importante, desta abordagem é quando g(Si) assume o valor 1 para
um subconjunto de estados e 0 para os demais estados. Nesse caso G(vk|O), equação
(2.6), é a probabilidade a posteriori do símbolo vk vir de um estado no conjunto
especificado.
Para implementar a solução baseada no segundo critério, o de encontrar a melhor
seqüência simples de estados, ou seja, encontrar o caminho mais provável, deve-se
53
maximizar P(Q|O,λ) ou, de forma equivalente, maximizar P(Q,O|λ). Desta forma tem-
se:
( )λ|,maxarg* OQPQQ
= . (2.7)
Para encontrar esse caminho Q* = q1* q2* ... qN*, com a mais alta probabilidade, para
uma certa seqüência de observações O, dado o modelo λ, recorre-se a uma técnica
formal baseada nos métodos de programação dinâmica, denominada algoritmo de
Viterbi. Nesse sentido define-se uma quantidade vt(i) que é a mais alta pontuação (mais
alta probabilidade) ao longo de um caminho até o instante t, referente às t primeiras
observações, terminando no estado Si.
( ) ( )λ|,max 2121121
ttqqqt OOOiqqqPivt
LLL
==−
(2.8)
Como o interesse reside, justamente, no melhor caminho através dos estados, então é
necessário um vetor ψt(j) para armazenar, para cada t e j, o argumento que maximiza
(2.8).
algoritmo de Viterbi.
1) Inicialização: ( ) ( )( )
=
≤≤=0
1 ,
1
11
iNiOeiv ii
ψπ
2) Recursão: ( ) ( )[ ] ( )( ) ( )[ ]
≤≤≤≤=
=
−≤≤
−≤≤ TtNjaivjOeaivjv
ijtNi
t
tjijtNit2 ,1, maxarg
,max
11
11ψ
3) Encerramento: ( )[ ]
( )[ ]
=
=
≤≤
≤≤
ivq
ivP
TNi
T
TNi
1
*1
*
maxarg
max
4) Recuperação do caminho: ( ) 1,,2,1 ,*11
* K−−== ++ TTtqq ttt ψ
54
2.2.2.3 Soluções para o Problema 3
A resolução do problema 3 consiste em determinar um método de ajuste (estimativa)
dos parâmetros do modelo λ = (A, E, π) que maximize a probabilidade da seqüência de
observações O para cada seqüência do conjunto de treinamento. Dado um conjunto de
seqüências (amostras) que se deseje modelar, usualmente referido como seqüências de
treinamento, para as quais pretende-se que o modelo ajuste bem, o processo de
treinamento consiste em estimar: a distribuição das observações para cada estado; todas
as probabilidades de transição entre estados; e as probabilidades iniciais para cada
estado.
Se os caminhos de estados são conhecidos para todas as seqüências de treinamento, os
parâmetros do modelo podem ser estimados computando-se seus valores esperados,
contando-se o número de vezes que cada transição (Aij) ou emissão (Ei(k)) ocorre no
conjunto de observações de treinamento e dividindo pela soma de todas as transições
ou de todas as emissões. Desta forma, as estimativas de máxima verossimilhança (ML)
para aij e ej(k) são dados por
( )( )∑∑
==
li
ii
lil
ijij
lEkEe
AA
a e . (2.9)
Quando os caminhos são desconhecidos não existe uma equação fechada para estimar
diretamente os valores dos parâmetros, de forma que, encontrar o melhor modelo λ
constitui um problema de otimização, ou seja, escolhe-se λ = (A, E, π) tal que a P(O|λ)
das seqüências do conjunto de treinamento sejam localmente maximizadas. Qualquer
um dos algoritmos padrões para otimização de funções contínuas pode ser usado,
entretanto, um método iterativo que tem uma interpretação probabilística natural, o
algoritmo de Baum-Welch (Durbin et al., 1998) é usado para escolher os parâmetros do
modelo λ.
Para descrever o procedimento de Baum-Welch para re-estimação (atualização e
melhoramento iterativos) dos parâmetros do HMM é necessário definir as fórmulas para
55
a re-estimação de A, E e π. Essas fórmulas são dadas através das equações em (2.9) e
(2.10), sendo que as equações em (2.10) são definidas em termos das variáveis forward-
backward, a seguir, onde L é o número de seqüências de treinamento.
( ) ( )( )
( ) ( ) ( ) ( )
( ) ( ) ( ) ( ) NjjbjfOP
kE
NjijbOeaifOP
A
NiOP
ibifT
T
vOt
lt
lt
L
llj
T
t
lt
ltjij
lt
L
llij
L
ll
ll
i
kt
≤≤=
≤≤=
≤≤=
∑∑
∑∑
∑
===
=++
=
=
1 ,|
1
,1 ,|
1
1 ,|
1
11
111
1
1
11
λ
λ
λπ
(2.10)
algoritmo de Baum-Welch
1) Inicialização: Leitura das seqüências de treinamento e adoção de uma estimativa
inicial para os parâmetros λ, geralmente tomando valores aleatórios para A, E e
π.
2) Re-estimação: O modelo corrente λ = (A, E, π) é usado para estimar, através das
equações em (2.9) e (2.10), os novos parâmetros do modelo ( )πλ ,, EA= .
3) Encerramento: O critério de parada, previamente definido, é verificado.
• Se foi atingido então faz-se λλ = e pára.
• Se não foi atingido faz-se λλ = e repete os passos 2 e 3.
O critério de parada é definido com base nas asserções dadas a seguir.
• “ou o modelo corrente λ define um ponto crítico da função de verossimilhança e,
nesse caso λλ = ;
• ou o modelo λ é mais provável que o modelo λ, no sentido que
( ) ( )λλ || OPOP > , ou seja, foi encontrado um novo modelo λ do qual as
56
seqüências de observações são mais prováveis de terem sido produzidas”
(Rabiner, 1989).
É importante salientar que o resultado final deste procedimento de re-estimação é uma
estimativa de máxima verossimilhança (MLE) do HMM, que o algoritmo forward-
backward, que é utilizado nas fórmulas em (2.10), somente leva a máximos locais e, que
a maioria dos problemas de interesse possuem superfície de otimização muito
complexa, com muitos máximos locais.
Uma abordagem alternativa largamente usada é denominada treinamento Viterbi. Nessa
abordagem os mais prováveis caminhos para as seqüências de treinamento são
derivados utilizando o algoritmo de Viterbi, e esses caminhos são usados no processo de
re-estimação dos parâmetros através das fórmulas em (2.9), para o caso de caminhos
conhecidos. O processo é iterado novamente quando novos valores de parâmetros são
obtidos. Nesse caso o algoritmo converge, precisamente, porque a atribuição de
caminhos é um processo discreto, e pode-se iterar até que nenhum dos caminhos mude
mais. Mas esse processo não maximiza a verdadeira verossimilhança, P(O|λ),
considerada como uma função dos parâmetros do modelo λ, ao contrário, ele encontra o
valor de λ que maximiza a contribuição dos mais prováveis caminhos, para todas as
seqüências de treinamento, para a verossimilhança. Talvez, por esse motivo a sua
performance não seja tão boa quanto a do algoritmo Baum-Welch, contudo é uma boa
opção quando o uso principal do HMM é produzir decifragens através de alinhamentos
Viterbi (Durbin et.al., 1998).
2.2.3 Modelo de Atribuição de Escores e Problemas Numéricos
A comparação de seqüências biológicas tem o propósito de evidenciar se elas têm
divergido de um ancestral comum por um processo de mutação e seleção natural. Nesse
processo a seleção natural tem o papel de filtragem das mutações, de forma que algumas
classes (tipos ou espécies) de mudanças possam ser vistas mais que outras. Os processos
mutacionais básicos freqüentemente considerados são substituições, adições e remoções
de resíduos numa dada seqüência. Inserções e remoções são referidas conjuntamente
como lacunas (gaps).
57
A suposição de que mutações ocorram independentemente em diferentes locais numa
seqüência, que trata uma lacuna de tamanho arbitrário como uma simples mutação,
parece ser uma aproximação razoável para seqüências de proteínas e de DNAs, apesar
de ser conhecido que as iterações entre resíduos têm papel decisivo na determinação
estrutural de DNAs e de proteínas. Contudo, essa suposição parece não ser apropriada
para RNAs, pois sabe-se que o pareamento de bases introduz dependências de longo
alcance nas estruturas de RNAs e, que muitos RNAs conservam mais as interações de
pareamento de base que produzem a sua estrutura secundária, do que a sua seqüência
primária de bases. Mas é bom lembrar que, apesar de ser possível levar em conta essas
dependências, isto ocasionaria complexidades computacionais significativas. Nos
alinhamentos de seqüências biológicas espera-se que identidades e substituições
conservadoras sejam observadas mais freqüentemente que mudanças aleatórias e, que
substituições não conservadoras sejam menos freqüentes que mudanças aleatórias.
Desta forma identidades e substituições conservadoras contribuem com termos de
escores positivos e as substituições não conservadoras contribuem com termos de
escores negativos.
Essa suposição de independência permite a construção e utilização de um sistema
aditivo de atribuição de escores, o que reduz bastante a complexidade computacional da
comparação de seqüências. Num tal esquema aditivo de atribuição de escores, o escore
total atribuído a um alinhamento é a soma dos termos para cada par alinhado de
resíduos mais os termos para cada lacuna (gap).
Além de encontrar um alinhamento ótimo é necessário avaliar a significância (ou
relevância) de seu escore, avaliar se o alinhamento é biologicamente significante
(relevante), fornecendo evidência para uma homologia, ou se é simplesmente o melhor
alinhamento entre duas seqüências não relacionadas. Para efetuar essa avaliação existem
basicamente duas abordagens, uma abordagem de estilo Bayesiano, baseada na
comparação de modelos diferentes e uma abordagem estatística tradicional baseada no
cálculo da probabilidade de um escore de concordância (match score) ser maior que o
valor observado, assumindo um determinado modelo nulo – o das seqüências
subjacentes não serem relacionadas. Para maiores detalhes veja Durbin et.al. (1998).
58
Nesse trabalho foi adotada a abordagem de tempero Bayesiano, pois o que se quer é a
probabilidade das seqüências serem relacionadas, dado o modelo alternativo M - o
modelo match - em oposição à probabilidade de não serem relacionadas, dado o
modelo aleatório R. O modelo aleatório R modela a probabilidade das seqüências
subjacentes serem não relacionadas. O modelo alternativo M, o modelo match modela a
probabilidade das seqüências em comparação serem relacionadas ou de uma seqüência
ser relacionada ao modelo M. A razão entre a probabilidade das seqüências serem
relacionadas e a probabilidade de não serem relacionadas é denominada taxa de
verossimilhança ou de disparidade (odds ratio). Para chegar a um sistema de atribuição
de escores positivos toma-se o logaritmo desta taxa, que é conhecido como taxa log-
odds. Para maiores detalhes veja Durbin et.al. (1998).
Na modelagem utilizando perfis-HMMs é construído um modelo probabilístico
explícito para um determinado conjunto de seqüências biológicas, onde os parâmetros
são específicos para cada posição no modelo e cada posição corresponde a uma coluna
num alinhamento múltiplo, logo para cada posição no modelo tem-se uma matriz de
probabilidades de emissão de símbolos associada. A solução adotada é a transformação
destas probabilidades de emissão de símbolos, dado o modelo alternativo M, o modelo
match, em escores log-odds relativos ao modelo aleatório R.
Na utilização destes algoritmos de PD em perfis-HMMs surgem preocupações com
relação à existência de problemas numéricos, pois mesmo nos mais modernos
processadores com suporte a operações de ponto flutuante poderia haver muitos
problemas numéricos quando computando os produtórios (multiplicando muitas
probabilidades) nos algoritmos de Viterbi, forward e backward, para modelos de
tamanhos realistas em muitas aplicações. Isto acontece porque os cálculos de ft(i), bt(i)
e vt(i) envolvem múltiplos termos menores do que 1 e maiores do que zero e, portanto,
ft(i), bt(i) e vt(i) tendem exponencialmente para zero, quando t torna-se muito grande.
Por exemplo, na modelagem de seqüências genômicas, para DNA, pode-se ter 100.000
bases ou mais. Supondo que as probabilidades de transição e emissão sejam tipicamente
da ordem de 0,1 (um décimo), a probabilidade do caminho Viterbi poderia ser da ordem
de 10 -100.000. Normalmente se recorre a duas diferentes formas de tratar esse problema.
59
Uma abordagem seria recorrer à transformação logarítmica de todas as probabilidades,
ou seja, no lugar das probabilidades sempre seria usado o logaritmo das probabilidades
e os produtórios seriam transformados desta maneira em somatórios. O uso de
transformações logarítmicas é mais freqüente e conveniente no cálculo das medidas de
desempenho através de um perfil-HMM já treinado. Essa transformação é,
freqüentemente, usada em conjunto com um sistema de atribuição de escores
denominado log-odds scoring.
Outra abordagem seria o scaling de probabilidades. Nesse caso, as variáveis de Viterbi,
forward e backward seriam re-escaladas (rescaled), de tal forma que elas permaneçam
dentro de um intervalo numérico gerenciável. Uma escolha conveniente seria definir
uma variável de scaling para fazer o somatório de todas as variáveis (Viterbi ou forward
ou backward) escaladas (scaled), num dado instante, igual a 1. Para maiores detalhes
ver (Rabiner, 1989) e (Durbin et.al., 1998). Essa abordagem é conveniente para ser
usada com o algoritmo de Baum-Whelch no treinamento de perfis-HMMs.
2.2.4 Perfis-HMMs para Seqüências Biológicas
Os HMMs podem assumir diversas topologias, sendo que na forma topológica mais
geral são permitidas transições de um estado para qualquer um dos outros estados,
assumindo uma topologia com estados totalmente interconectados.
Na modelagem de HMMs, é possível iniciar o treinamento do HMM a partir desta
topologia mais geral e deixar que convirja para um modelo ótimo, mas geralmente essa
estratégia tem se mostrado infrutífera devido à convergência para mínimos locais e ao
número excessivo de parâmetros a serem considerados.
Na prática HMMs, bem sucedidos, são construídos com base no conhecimento sobre o
problema sendo investigado, decidindo cuidadosamente quais e quantos estados o
modelo deve ter e quais transições serão permitidas entre esses estados. Portanto a
modelagem de HMMs é mais uma arte do que uma técnica e está fortemente ligada ao
problema, ou seja, às propriedades do sinal que está sendo modelado. Desta forma, será
60
tratada, nos parágrafos seguintes, da modelagem de seqüências biológicas através de
HMMs.
De maneira informal pode-se dizer que as proteínas são construídas de um alfabeto de
20 pequenas moléculas conhecidas como aminoácidos. Dentro de uma proteína a
ligação entre dois aminoácidos é conhecida como uma ligação peptídica. Uma
seqüência linear de aminoácidos numa cadeia polipeptídica é conhecida como a
estrutura primária da proteína, podendo ser formadas 20n cadeias polipeptídicas, com n
aminoácidos. Entretanto, dentro destas cadeias polipeptídicas certas seções podem ser
torcidas em forma de espirais e/ou entrelaçadas em forma de chapas, laços e hélices.
Essas formas são conhecidas como a estrutura secundária das proteínas e são formadas
por pontes de hidrogênio entre dois aminoácidos não adjacentes na cadeia polipeptídica.
Embora a cadeia linear de aminoácidos seja uma visão simplificada da estrutura de uma
molécula de proteína, relações entre proteínas podem ser inferidas somente comparando
suas estruturas primárias. É sob está ótica, e desconsiderando qualquer dependência
entre os aminoácidos da cadeia, que foram propostos os HMMs lineares para
modelagem de seqüências biológicas. Entretanto, as dependências entre os aminoácidos
dentro de uma proteína tem motivado pesquisas em outras classes de modelos
estatísticos que possam capturá-las, tais como modelos híbridos de redes neurais e
HMMs, redes Bayesianas dinâmicas, HMMs fatoriais, árvores de Boltzmann e campos
aleatórios markovianos ocultos (Durbin et al., 1998).
Os HMMs comumente utilizados para a modelagem de seqüências biológicas são do
tipo com transições somente da esquerda para a direita (left-right) e são referidos, no
campo da Bioinformática, como HMMs lineares e/ou perfis-HMMs, devido ao arranjo
dos estados em forma linear. Esse arranjo linear dos estados de um perfil-HMM foi
motivado por uma técnica ordinária de modelagem biológica denominada “perfil”, que
alinha seqüências a uma seqüência de consensu. Desta maneira um HMM linear para
uma seqüência de aminoácidos (proteínas) ou de nucleotídeos (RNAs e DNAs) é um
conjunto de posições que correspondem aproximadamente a colunas num alinhamento
de múltiplas seqüências.
61
Nos perfis-HMMs utilizados na modelagem de seqüências biológicas existem três tipos
de estados: match (de concordância), insert (de inserção ou de adição) e delete (de
remoção). Os estados denominados match correspondem à letras da seqüência de
consensu. Os estados denominados insert correspondem à posições entre as letras da
seqüência de consensu onde lacunas (gaps) podem ser abertas e letras inseridas. Os
estados denominados delete permitem saltar uma coluna sem emitir (gastar) nenhuma
letra. A topologia básica do modelo é mostrada na Figura 2.2. Os estados denominados
match, insert e delete são representados por retângulos, trapézios e círculos,
respectivamente, e estão agrupados em módulos (blocos ou nós) correspondendo a uma
posição na seqüência de consensu. A cada linha conectando dois estados existe uma
probabilidade de transição associada. É importante ressaltar a existência de dois
módulos especiais, um modelando o início e outro o final do modelo. Outras topologias
podem ser consideradas, mas por razões de eficiência a topologia mostrada na Figura
2.2 normalmente é utilizada. O tamanho do modelo refere-se ao número de nós no
HMM (com exceção dos módulos especiais) ou, equivalentemente, ao número de
estados denominados match no HMM. Esse modelo apresenta transições entre os
estados insert e delete, pois apesar delas, usualmente, serem improváveis, a ausência
delas pode trazer problemas na fase de construção do modelo (treinamento).
FIGURA 2.2 - Topologia de um perfil-HMM com 4 módulos padrões. FONTE: Hughey e Krogh (1996).
Os perfis-HMMs são ferramentas poderosas e confiáveis para a modelagem de uma
família de seqüências biológicas não-alinhadas de aminoácidos ou de nucleotídeos, ou
para modelar domínios ou fragmentos especiais (motifs) comuns a um conjunto de
seqüências biológicas não-alinhadas. O perfil-HMM resultante da modelagem (treinado)
62
pode, então, ser usado para comparação de uma seqüência ao HMM, para busca ou para
alinhamento múltiplo. A seguir são dados versões log-odds dos algoritmos forward e
Viterbi desenhados especialmente para perfis-HMMs.
Nessas versões log-odds dos algoritmos forward e Viterbi desenhados especialmente
para perfis-HMMs, usualmente, assume-se que a distribuição das probabilidades de
emissão dos estados Ij é a mesma distribuição do modelo nulo usado com escores em
log-odds. Nesse caso, essas probabilidades se anulam produzindo um escore nulo:
01log)(
log ==t
j
O
tI
q
Oe.
As versões log-odds dos algoritmos forward e Viterbi apresentadas nessa seção são
baseadas numa arquitetura básica para perfis-HMMs, conforme a Figura 2.2,
apresentam transições entre os estados insert e os estados delete, mas elas podem ser
suprimidas se não estão presentes nos modelos construídos, por serem bastante
improváveis.
Seja o escore em log-odds do melhor caminho concordando (matching) a
subseqüência O
)(tV Mj
1O2O3...Oi ao sub-modelo até o bloco j, terminando com Oi sendo
emitido pelo estado Mj. Similarmente, seja V o escore do melhor caminho
terminando com O
)(tIj
i sendo emitido pelo estado Ij. Seja, de forma análoga, V o escore
para o melhor caminho terminando no estado D
)(tDj
j. O algoritmo de Viterbi para perfis-
HMMs usando log-odds é dado a seguir.
algoritmo de Viterbi para perfis-HMMs usando log-odds
1) Inicialização:
( ) 000 =MV
2) Recursão:
63
+
+
+
=
+−
+−
+−
+=
+−
+−
+−
+=
−
−
−
−
−
−
−
−
−
−
−
−
log)(log)(log)(
max)(
log)1(log)1(log)1(
max)(
log)(
log)1(log)1(log)1(
max)(
log)(
1
1
1
1
1
1
1
1
1
1
1
1
jj
jj
jj
jj
jj
jj
t
j
jj
jj
jj
t
j
DDDj
DIIj
DMMj
Dj
IDDj
IIIj
IMMj
O
tIIj
MDDj
MIIj
MMMj
O
tMMj
atVatVatV
tV
atVatVatV
qOe
tV
atVatVatV
qOe
tV
3) Encerramento:
+++
=+
+
+
+
+ log)(log)(log)(
max)1(
1
1
1
1
NN
NN
NN
MDD
N
MII
N
MMM
NM
N
aTVaTVaTV
TV
De forma análoga às equações de recorrência para o algoritmo de Viterbi com escores
em log-odds, sejam as variáveis para os escores parciais das taxas
totais em log-odds correspondendo a V . O algoritmo forward para
perfis-HMMs usando log-odds é dado a seguir. Embora pareça complicado, a operação
)( e )(),( tFtFtF Ij
Mj
Dj
e )(),( tVt Mj
Dj )(tV I
j
( )yx ee +log pode ser efetuada eficientemente, numa implementação prática, para uma
precisão adequada (aceitável) através de interpolação e de uma tabela de consulta
(lookup).
algoritmo forward para perfis-HMMs usando log-odds
1) Inicialização:
( ) 000 =MF
2) Recursão:
64
( )( ) ( )( ) ( )( )
( )( )
( )( ) ( )( ) ( )( )
( )( )( ) ( )( ) ( )( ) ( )( )[ ]
++=
−+
+−+−+=
−+
+−+−+=
−−−
−
−−
−−−
−
−−
tFatFatFatF
tFa
tFatFa
q
OetF
tFa
tFatFa
q
OetF
DjDD
IjDI
MjDM
Dj
DjID
IjII
MjIM
O
tIIj
DjMD
IjMI
MjMM
O
tMMj
jjjjjj
jj
jjjj
t
j
jj
jjjj
t
j
111
1
11
expexpexplog
1exp
1exp1exploglog
1exp
1exp1exploglog
111
1
11
3) Encerramento:
( ) ( )( ) ( )( ) ( )( )[ ]TFaTFaTFaTF DNMD
INMI
MNMM
MN NNNNNN
expexpexplog11111 +++
++=++
O processo de modelagem usando perfis-HMMs é essencialmente a construção de um
modelo que represente a seqüência de consensu para uma determinada família (ou
conjunto) de seqüências, pois não há nenhum interesse em construir um modelo para
representar qualquer seqüência em particular. Assim a estrutura apresentada na Figura
2.2 é usada como um arcabouço inicial no treinamento de perfis-HMMs, determinando-
se, então, as probabilidades de transição e de emissão de símbolos de tal forma que elas
capturem informação específica sobre cada posição num múltiplo alinhamento da
família ou conjunto de seqüências.
Desta forma, o processo de treinamento de um HMM consiste tipicamente em, dado um
modelo inicial, fazer várias computações completas da matriz de PD em cada seqüência
do conjunto de treinamento e, então, re-estimar os parâmetros do modelo. Esse processo
é repetido várias vezes até que o modelo convirja para uma representação estatística da
família de seqüências representativa da amostra ou conjunto de treinamento. O
treinamento de perfis-HMMs de seqüências não-alinhadas é feito com o algoritmo de
Baum-Welch (forward-backward), usando todos os caminhos de estados, ou
alternativamente com o algoritmo de Viterbi, usando os caminhos de estados mais
prováveis. A seguir são dados os algoritmos forward e backward usados para o
treinamento de perfis-HMMs e as equações para re-estimação dos parâmetros dos
65
perfis-HMMs. O algoritmo de Viterbi é praticamente semelhante ao algoritmo forward,
só que esse maximiza a cada passo ao invés de somar sobre todos os caminhos.
algoritmo forward usado no treinamento de perfis-HMMs
1) Inicialização:
( ) 100
=Mf
2) Recursão:
( ) ( ) ( ) ( ) ( )[ ]( ) ( ) ( ) ( ) ( )[ ]( ) ( ) ( ) ( )
++=
−+−+−=
−+−+−=
−−−−−−
−−−−−−
jjjjjjjjjj
jjjjjjjjjjj
jjjjjjjjjjj
DDDDIIDMMD
IDDIIIIMMtII
MDDMIIMMMtMM
atfatfatftf
atfatfatfOetf
atfatfatfOetf
111111
111111
111
111
3) Encerramento:
( ) ( ) ( ) ( )1111
1++++
++=+NNNNNNNNNN MDDMIIMMMM aTfaTfaTfTf
algoritmo backward usado no treinamento de perfis-HMMs
1) Inicialização:
( )( )
( )( )
=
=
=
=+
+
+
+
+
1
1
1
111
NNN
NNN
NNN
N
MDD
MII
MMM
M
aTb
aTb
aTb
Tb
2) Recursão:
( ) ( ) ( ) ( ) ( ) ( )( ) ( ) ( ) ( ) ( ) ( )( ) ( ) ( ) ( ) ( ) ( )
++++=
++++=
++++=
+++++
+++++
+++++
++
++
++
11111
11111
11111
11
11
11
11
11
11
jjjjjjjjjjjj
jjjjjjjjjjjj
jjjjjjjjjjjj
DDDtIIDItMMDMD
DIDtIIIItMMIMI
DMDtIIMItMMMMM
atbOeatbOeatbtb
atbOeatbOeatbtb
atbOeatbOeatbtb
equações de re-estimação usadas no treinamento Baum-Welch para perfis-HMMs
66
( ) ( ) ( ) ( )
( ) ( ) ( ) ( )
( ) ( ) ( ) ( )
( ) ( ) ( ) ( )
( ) ( ) ( )∑∑
∑∑
∑∑
∑∑
∑∑
==
=+
=
=+
=
===
===
+++
++++
=
+=
+=
=
=
T
t
lDDX
lX
L
llDX
T
t
lI
ltIIX
lX
L
llIX
T
t
lM
ltMMX
lX
L
llMX
T
vOt
lI
lI
L
llI
T
vOt
lM
lM
L
llM
tbatfOP
A
tbOeatfOP
A
tbOeatfOP
A
tbtfOP
kE
tbtfOP
kE
jjjjjj
jjjjjjj
jjjjjjj
kt
jjj
kt
jjj
11
11
1
11
1
11
11
111
1111
|1
1|
1
1|
1
|1
|1
λ
λ
λ
λ
λ
(2.11)
Uma vantagem significativa de se usar HMMs na modelagem de seqüências biológicas
deve-se a eles poderem ser automaticamente treinados ou estimados de seqüências não-
alinhadas. Entretanto em aplicações práticas na modelagem de seqüências biológicas
surgem vários problemas que precisam ser contornados. Esses problemas são
enumerados a seguir.
O algoritmo de Baum-Welch utilizado no treinamento é uma técnica de otimização hill-
climbing (subir encosta) que freqüentemente encontra soluções sub-ótimas, resultando
em modelos inferiores. As heurísticas utilizadas para contornar esse problema
aumentam o tempo de computação, que pode ser considerável se for levado em conta
que seqüências biológicas podem conter de centenas a milhões de letras.
É necessário recorrer a uma heurística para determinar o tamanho do modelo, pois o que
se quer é modelar a estrutura destas seqüências, que freqüentemente não estão
alinhadas. Essa heurística consiste de um processo iterativo de cirurgia (surgery) e re-
estimação do modelo, que se repete até a sua estabilização. Isto acresce carga
computacional ao treinamento do HMM.
Freqüentemente os dados de treinamento não constituem uma amostra representativa da
família de seqüências que está sendo modelada, ou por existirem poucos dados ou por
estes dados serem viciados - por conterem um grande número de seqüências homólogas
muito próximas e/ou de duplicatas exatas de seqüências. Esse não é somente um
67
problema teórico onde essa classe de dados distorcidos conflita com a suposição de
independência entre os dados, pois estes dados viciados podem restringir as capacidades
do modelo treinado. O modelo resultante pode ter pouca capacidade de generalização -
com desempenho muito pobre nas atividades de busca e discriminação em bancos de
dados - e/ou pode ter dificuldade de distinguir posições mais conservadas de posições
menos conservadas.
2.3 Medidas de Complexidade de Algoritmos
Essa seção trata de algoritmos e do desempenho destes; das medidas de eficiência e de
complexidade. Na seção 2.3.1 apresenta-se uma definição de algoritmo. Na seção 2.3.2
são tratadas a análise de desempenho de algoritmos e a complexidade de problemas.
2.3.1 Definição de Algoritmo
O termo algoritmo é proveniente da palavra árabe alkharizmi, que por sua vez é
originária do nome do matemático persa do século IX Abu Ja’far Mohammed ibn Musa
al Khowarizmi (Knuth, 1973).
Kowalski (1979) entende que algoritmos convencionais são compostos por dois
elementos. O primeiro é o elemento lógico ou modelo, que consiste do conhecimento
sobre o problema em estudo. O segundo é o elemento responsável pelo controle, que
consiste da metodologia de aplicação do conhecimento para resolver o problema.
Um algoritmo pode ser visto como uma receita matemática para resolver um dado
problema ou uma determinada classe de problemas. Essa receita deve ser precisa,
correta e detalhada como uma construção teórica no campo da matemática. Um
algoritmo descreve um padrão de comportamento, expresso através de um conjunto
finito de ações ou instruções, que podem ser seguidas (realizadas, efetuadas,
executadas) para se obter a solução de uma determinada classe de problemas.
Segundo Toscani e Veloso (2001) algoritmos são o cerne da computação: um programa
codifica um algoritmo de modo a ser executatado em um computador, resolvendo assim
um problema. Um algoritmo é um procedimento, consistindo de um conjunto de regras
68
não ambíguas, as quais especificam, para cada entrada, uma seqüência finita de
operações, terminando com uma saída correspondente. Na verdade, um algoritmo
resolve um problema quando, para qualquer entrada, produz uma resposta correta, se
forem concedidos tempo e memória suficientes para sua execução. Entretanto, o fato de
um algoritmo resolver (teoricamente) um problema não significa que seja aceitável na
prática. Os recursos de espaço e de tempo requeridos têm grande importância em casos
práticos, ou seja, na prática é fundamental que um programa produza a solução com
dispêndio de tempo e de memória razoáveis, o que ressalta a importância da análise de
complexidade, dos métodos de projeto e da análise de algoritmos.
Para Resende e Stolfi (1994) o grau de detalhe e formalismo de um dado algoritmo
depende do “receptor” almejado. Se o receptor é um programador, um engenheiro ou
um cientista usa-se uma linguagem semi-formal, semelhantes àquelas usadas em
construções lógicas ou matemáticas, e omite-se, geralmente, todos os detalhes que
podem ser facilmente supridos pelo receptor em questão. Caso o receptor seja um
computador, todos os detalhes devem ser especificados através de uma representação
(notação) que possa ser interpretada e seguida mecanicamente pelo computador. Os
algoritmos que podem ser avaliados por computadores são, freqüentemente,
denominados programas.
Segundo Resende e Stolfi (1994) um algoritmo deve conter os seguintes elementos:
1) Enunciado – o enunciado descreve os dados a serem fornecidos (entrada) ao
algoritmo, os resultados calculados (saída) por esse e a relação entre dados e
resultados.
2) Modelo Computacional – o modelo computacional consiste na definição das
operações “elementares” permitidas e do custo de cada uma delas. O modelo
computacional permite computar a eficiência (ou ineficiência) de cada solução.
Essas operações “elementares” são operações que o executor do algoritmo
supostamente saiba executá-las; no caso do computador são aquelas que podem
ser efetuadas com um número fixo de instruções básicas. Para mais detalhes
sobre modelos computacionais veja Leeuwen e Wiedermann (2000).
69
3) Seqüência de operações – é uma especificação da seqüência de operações que
devem ser efetuadas. Essa seqüência de operações é dependente dos dados de
entrada, que podem variar infinitamente. Desta maneira a notação (os comandos
e as estruturas de operação) adotada precisa descrever de maneira finita esta
infinidade de seqüências.
4) Prova de correção – um algoritmo precisa satisfazer às seguintes condições, para
que possa ser usado com confiança.
• As operações previstas precisam ser sempre válidas e definidas,
quaisquer que sejam os dados fornecidos ao algoritmo.
• O algoritmo sempre termina após efetuar um número finito de operações
previstas.
• A saída do algoritmo precisa satisfazer as condições estabelecidas no
enunciado deste.
5) Análise de desempenho – é necessário efetuar uma análise de desempenho para
a comparação dos algoritmos diferentes, desenhados para resolver um dado
problema, o que permite fazer uma escolha apropriada do algoritmo para a
resolução do problema.
2.3.2 Análise de Desempenho de Algoritmos e Complexidade de Problemas
Algoritmos podem ser analisados ou comparados sob vários pontos de vista tais como:
flexibilidade, robustez, facilidade de uso, tratamento de dados incorretos, eficiência,
complexidade, custo. Entretanto, dentre esses pontos de vista, o do custo ou esforço
computacional do algoritmo tem sido, freqüentemente, adotado devido à facilidade de
quantificação e, portanto, de ser estudado cientificamente. Esse custo ou esforço
computacional do algoritmo, freqüentemente, é denominado de eficiência ou
complexidade do algoritmo.
70
Para Toscani e Veloso (2001) o crescente avanço tecnológico, permitindo a criação de
máquinas cada vez mais rápidas, pode, ingenuamente, parecer ofuscar a importância da
complexidade de tempo de um algoritmo. Um raciocínio semelhante pode ser feito com
relação à importância da complexidade de espaço. Entretanto, acontece exatamente o
inverso, as máquinas, tornando-se cada vez mais rápidas e mais poderosas em termos de
recursos de memória, passam a poder resolver problemas maiores, e é a complexida de
tempo e de espaço do algoritmo que determina o novo tamanho máximo de problema
resolvível. Para um algoritmo eficiente, qualquer melhoria, na velocidade de execução
das operações básicas e nos recursos de memória, é sentida, e o conjunto de problemas
resolvíveis por ele aumenta sensivelmente. Esse impacto é menor no caso de algoritmos
menos eficientes.
Em Kearney et al. (1986) a complexidade é entendida como uma medida dos recursos
gastos por um sistema em sua interação com uma peça de software para executar uma
dada tarefa. Se o sistema é um computador então complexidade é definida pelo tempo
de execução e pelos requerimentos de armazenagem (memória) necessários para
efetuar a computação. Geralmente, essas medidas são baseadas no código do programa,
suprimindo-se comentários e atributos estilísticos, e dependem do tamanho do
programa, da estrutura de controle ou da natureza do módulo de interfaces. Entretanto,
ele observa, que existem fatores externos ao programa (ou cógigo do programa) que
contribuem para a complexidade do programa (ou algoritmo).
A análise da eficiência ou complexidade de algoritmos pode ser feita através da
realização de estimativas a priori e/ou através da realização de testes a posteriori. Essas
duas metodologias de análise do esforço computacional são igualmente importantes e
não são necessariamente contrapostas, mas complementares. A análise a priori de
algoritmos é feita através de medidas comparativas teóricas de ordem de complexidade,
que são utilizadas para comparar algoritmos cujos esforços computacionais diferem por
ordens de magnitude. Já a análise a posteriori é utilizada para comparar algoritmos
semelhantes em conceito através de medidas empíricas, realizadas a posteriori, do
desempenho de versões dos algoritmos já implementadas e compiladas para uma
determinada arquitetura.
71
Segundo Toscani e Veloso (2001) a análise da complexidade de um algoritmo é
realizada, usualmente, de maneira muito particular, o que é compreensível, já que a
complexidade é uma medida que tem parâmetros bem particulares do algoritmo. Apesar
disso, alguns conceitos são gerais, no sentido de que só dependem da estrutura do
algoritmo. Existem vários critérios de avaliação de um algoritmo como: quantidade de
trabalho requerido, quantidade de espaço requerido, simplicidade, exatidão de resposta e
otimalidade. Várias também são as medidas de complexidade: por exemplo, tempo e
espaço requeridos por um algoritmo, relacionadas à velocidade e à quantidade de
memória, respectivamente.
Nesse trabalho a análise de desempenho de algoritmos é tratada do ponto de vista do
custo ou esforço computacional, ou seja, do uso de recursos requeridos por um
algoritmo. Recursos típicos de interesse primário incluem entre outros: memória, tempo
computacional, banda de comunicação e portas lógicas. Entretanto, segundo Resende e
Stolfi (1994), na maioria das vezes, os recursos mais importantes a serem analisados são
o tempo gasto na execução do algoritmo e/ou o espaço de armazenamento ocupado
pelas variáveis internas. É bom lembrar que, tipicamente, a análise do desempenho de
algoritmos e da complexidade de problemas é feita em função do tamanho da entrada do
problema.
Segundo Resende e Stolfi (1994) o tempo de execução, T, de um programa (algoritmo)
é afetado por vários fatores, tais como o modelo computacional, o computador real
usado, o algoritmo adotado, os detalhes da codificação do algoritmo, o compilador
(tradução para a linguagem de máquina do computador) e a entrada do algoritmo. Dessa
forma, o tempo exato de execução, T, geralmente não pode ser determinado a partir da
descrição semi-formal do algoritmo. Entretanto, pode-se obter uma estimativa do tempo
de execução T a partir da contagem do número de operações elementares, l, efetuadas
pelo mesmo. Considerando que cada operação elementar, l, pode ser traduzida num
número fixo de instruções de máquina, tem-se que essa estimativa é aproximadamente
proporcional ao tempo de execução T, pois existem constantes a1, b1, a2, b2 tais que
. Observa-se que devido ao fato destas constantes (a2211 blaTbla +≤≤+ 1, b1)
dependerem da máquina e do programador, não é muito relevante obter o número exato
72
de operações l, nem considerar todas as operações elementares previstas no algoritmo,
basta considerar apenas aquelas mais relevantes para o cálculo do custo e a maneira
como o número de operações elementares l varia com o tamanho do problema. Assim,
segundo Resende e Stolfi (1994) o desempenho de um algoritmo é expresso através de
uma função cujo parâmetro é o tamanho da entrada deste.
Segundo Toscani e Veloso (2001) para medir a quantidade de trabalho realizada por um
algoritmo, é escolhida uma operação, chamada operação fundamental, e então é contado
o número de execuções dessa operação na execução do algoritmo. A operação escolhida
como fundamental deve ser tal que a contagem do número de vezes que ela é executada
expresse a quantidade de trabalho do algoritmo, dispensando outras medidas. Às vezes,
é necessário mais do que uma operação fundamental, inclusive com pesos diferentes.
Algumas conclusões sobre o espaço utilizado podem ser tiradas, examinando-se o
algoritmo. Um programa requer uma área para guardar suas instruções, suas constantes,
suas variáveis e os dados. Pode também utilizar uma área de trabalho para manipular os
dados e guardar informações para levar adiante a computação. Se os dados têm uma
representação natural, como uma matriz, são considerados para análise de espaço
somente o espaço extra, além do espaço utilizado para para guardar as instruções do
programa e os dados. Porém, se os dados podem ser representados de várias formas, por
exemplo como um grafo, então o espaço requerido para guardá-los também deve ser
levado em conta.
Denomina-se cota superior (da complexidade) de um problema a qualquer função que
expresse o esforço computacional (eficiência) de um algoritmo que o resolve, em
função do tamanho da entrada. Sendo que, freqüentemente, o objetivo de um projetista
de algoritmos é o de obter a menor cota superior possível (Resende e Stolfi, 1994).
Para Toscani e Veloso (2001) a complexidade também pode ser vista como uma
propriedade do problema, o que significa dar uma medida independente do tratamento
dado ao problema, independente do caminho percorrido na busca da solução, portanto
independente do algoritmo. A complexidade de um problema é a do melhor algoritmo
imaginável (conhecido ou não) que possa resovê-lo. Dessa forma, segundo Resende e
73
Stolfi (1994), denomina-se cota inferior (da complexidade) ou complexidade assintótica
de um problema a qualquer função que expresse o número mínimo de operações que
precisa ser realizado para qualquer solução do problema, em função do tamanho da
entrada. Sendo que, freqüentemente, o objetivo de um analista da complexidade de
problemas é o de obter a maior cota inferior possível.
A cota superior (da complexidade) de um problema ou complexidade assintótica de um
algoritmo pode ser obtida através da análise do pior caso do tempo de execução de um
dado algoritmo, que é o maior tempo de execução do algoritmo para qualquer entrada
de tamanho n. A análise da complexidade assintótica de um algoritmo, geralmente, é
reduzida à análise do pior caso do tempo de execução, devido às seguintes
considerações:
• O pior caso do tempo de execução de um algoritmo fornece uma estimativa
razoável do limite superior do tempo de execução para qualquer entrada, o que
propociona confiança ou garantia de que o algoritmo nunca vai gastar mais
tempo para a execução do que esse tempo de pior caso.
• Quase sempre é necessário fazer uma estimativa sobre o tempo de execução do
algoritmo e espera-se que este nunca tenha um desempenho pior.
• Para alguns algoritmos o pior caso de desempenho ocorre com muita freqüência.
Em determinados casos o interesse recai sobre a análise do caso médio (ou esperado) do
tempo de execução de um algoritmo, pois um algoritmo pode ter um comportamento
médio bastante moderado em termos de uso de recursos computacionais, apesar do
comportamento de pior caso ser bastante oneroso computacionalmente. Mas está análise
do caso médio, freqüentemente, é difícil de ser efetuada, pois nem sempre está claro o
que constitui uma entrada “média” para um dado problema ou qual a distribuição de
probabilidades das entradas.
74
2.3.2.1 Análise Assintótica
Freqüentemente o comportamento do crescimento de uma dada função fica mascarado
por constantes multiplicativas e outros termos de menor grau. Adicionalmente uma
função pode apresentar um comportamento para entradas de tamanhos limitados, que
não corresponde à tendência de seu comportamento para entradas de tamanho crescente.
Desta forma, tanto a comparação da eficiência de algoritmos para resolver um dado
problema, quanto a comparação da complexidade de diferentes problemas, assim como
a determinação da complexidade de um dado problema requerem a comparação do
crescimento assintótico de funções, se faz necessário o estabelecimento de uma notação
mais formal e concisa.
Sejam f e g: N → N funções positivas. Se o limite do quociente )()( nfng quando n
tende ao infinito é definido, pode-se classificar f e g quanto a seus crescimentos
assintóticos, com base no valor deste limite (zero, positivo finito, infinito). Deve ser
observado que as funções g(n) e f(n) são destituídas de constantes multiplicativas bem
como de termos de menor grau.
Notação big-O: Diz-se que um algoritmo tem ordem de complexidade
se existem constantes, c e n))(()( nfOng = 0, tais que 0 )()( nfcng ⋅≤≤ , para ,
ou seja, se
0nn ≥
∞<)()(
nn
∞→ fg
nlim . Essa definição diz que f(n) domina assintoticamente g(n), ou
seja, a taxa de crescimento de g(n) é menor que ou igual à taxa de crescimento de f(n).
Isto significa que f(n) é um limite superior para g(n).
Notação big-Ω: se existem duas constantes, c e n))(()( nfng Ω=
0n≥
0, tais que
, para n , ou seja, se 0)()( ≥⋅≥ nfcng 0)()(lim >
∞→ nfng
n. Essa definição assegura que
f(n) seja um limite inferior para g(n), ou seja, a taxa de crescimento de g(n) é maior que
ou igual à taxa de crescimento de f(n).
75
Notação big-Θ: se, e somente se, ))(()( nfng Θ= ))(()( nfOng = e , ou
seja, existem três constantes, c
))(()( nfng Ω=
0)(2 ≥⋅≥1, c2 e n0, tais que )()(1 ≥⋅ nfcngnfc , para
. Essa definição diz que a taxa de crescimento de g(n) é igual à taxa de
crescimento de f(n).
0nn ≥
Notação little-o: se ))(()( nfong = 0)()(lim =
∞→ nfng
n, ou seja, se e
. Para qualquer constante c > 0 existe uma constante n
))(()( nfOng =
))(()( nfng Θ≠
()(0 nfcng ⋅<≤
0 tal que
, para . Essa definição diz que a taxa de crescimento de g(n) é
menor que a taxa de crescimento de f(n), enquanto que a definição big-O permite que a
taxa de crescimento de g(n) seja igual à taxa de crescimento de f(n).
) 0nn ≥
Notação little-ω: ))(()( nfng ω= se ∞=∞→ )(
)(limnfng
n
)()(
, ou seja, se para qualquer constante
c > 0 existe uma constante n0 tal que 0≥⋅> nfcng , para n . Essa definição diz
que a taxa de crescimento de g(n) é maior que a taxa de crescimento de f(n), enquanto
que a definição big-Ω permite que a taxa de crescimento de g(n) seja igual à taxa de
crescimento de f(n).
0n≥
Para provar a ordem de complexidade de uma determinada função g(n) ou comparar a
ordem de complexidade de várias funções e para provar a complexidade de um
determinado problema ou comparar a complexidade de diversos problemas, usualmente,
não se aplica estas definições formalmente, mas uma coleção de resultados e regras
conhecidos, que são enumerados a seguir. Isto significa que a prova ou a determinação
de que a suposição seja correta, geralmente, é uma tarefa muito simples e não deveria
envolver Álgebra ou Cálculo.
Primeiro. Com base nas definições de complexidade assintótica pode ser observado
que:
• se e somente se ))(()( nfOng = ))(()( ngnf Ω=
76
• se e somente se ))(()( nfng Ω= ))(()( ngOnf =
• se e somente se ))(()( nfong = ( ) ( )( )ngnf ω=
• ))(()( nfng ω= se e somente se ))(()( ngonf =
• se e somente se ))(()( nfng Θ= ))(()( ngnf Θ=
Segundo: Se h1(n) = O(f(n)) e h2(n) = O(g(n)), então
• h1(n) + h2(n) = max (O(f(n)), O(g(n))),
• h1(n) * h2(n) = O(f(n) * g(n)),
Terceiro: Se p(x) é um polinômio de grau n, então p(x) = . )( nxΘ
Quarto: logk n = O(n) para qualquer constante k. Isto implica que logaritmos crescem
muito lentamente.
Quinto: A Tabela 2.1 contém as principais funções dispostas por ordem crescente de
complexidade assintótica, big-O. O esboço gráfico da maioria delas é apresentado na
Figura 2.3.
TABELA 2.1 – Taxas de Crescimento Típicas.
Função Nome
c Constante
logn Logarítmica
log2n Logarítmica-quadrada
n Linear
n log n Linearítmica
n2 Quadrática
n3 Cúbica
2n Exponencial
77
FIGURA 2.3 – Taxa de crescimento do tempo de computação de funções típicas.
2.4 Algoritmos de Programação Dinâmica
Essa seção trata do método PD que é muito utilizado em projetos de algoritmos. Faz a
conceituação e a descrição do método de PD e trata de sua aplicação na comparação de
seqüências biológicas através dos HMMs. A Seção 2.4.1 trata da conceituação e
descrição do método de PD. Seção 2.4.1 trata da aplicação desse método de PD aos
HMMs.
2.4.1 Estratégia de Programação Dinâmica
Segundo Toscani e Veloso (2001) a PD é um metodo iterativo, ascendente, que costuma
ser aplicado a determinados problemas de otimização. O método de PD opera,
decompondo um problema em subproblemas mínimos, solucionando os subproblemas,
guardando os resultados parciais, combinando subproblemas menores e sub-resultados
para obter e resolver problemas maiores, até recompor e resolver o problema original.
A PD é aplicável aos problemas de otimização com a seguinte propriedade de
otimalidade: “uma solução não ótima de um subproblema não pode ser subsolução de
solução ótima do problema”. Isto significa que só soluções ótimas dos subproblemas
podem compor uma solução ótima do problema. Em conseqüência, na procura de uma
78
solução ótima, as soluções não ótimas de subproblemas podem ser descartadas, e é esse
o processo que torna os algoritmos desenvolvidos por PD mais eficientes que os
algoritmos diretos. Além dissso, os subproblemas são resolvidos uma única vez, e suas
soluções são guardadas para serem usadas tantas vezes quantas forem necessárias
(Toscani e Veloso, 2001).
A programação dinâmica é conveniente para resolver problemas de otimização com
uma estrutura iterativa de decisões seqüenciais que apresentem as seguintes
características:
1) Possuem subestruturas ótimas. Uma solução ótima para uma instância do
problema contém uma solução ótima para suas sub-instâncias;
2) Podem ser divididos em subproblemas, que correspondem a essas intâncias ou
sub-instâncias;
3) Os subproblemas são sobrepostos (dependentes). Permitindo que as mesmas
sub-instâncias sejam referidas repetidamente durante a recursão.
2.4.2 Métodos de Programação Dinâmica e HMMS
A abordagem algorítmica empregando PD em Bioinformática apresenta os seguintes
componentes:
1) Uma relação de recorrência para definir o valor de uma solução ótima para
determinados prefixos das seqüências, usando soluções prévias de prefixos
menores;
2) Uma computação tabular (em forma matricial) para computar o valor de uma
solução ótima;
3) Uma rotina ou procedimento de retrocedimento (traceback) sobre a matriz de
PD para emitir (ou recuperar) uma solução ótima.
79
A comparação de seqüências biológicas usando PD é organizada comparando primeiro
subseqüências menores e armazenando os custos destas comparações numa tabela para
serem usados nas próximas comparações de subseqüências maiores, até que sejam
comparadas as seqüências inteiras. A entrada final da tabela armazena o resultado ou
estimativa da comparação global.
A abordagem de PD constitui o corpo principal de praticamente todos os algoritmos e
métodos utilizados para a comparação de seqüências biológicas. Dentre estes algoritmos
e modelos podem ser relacionados os seguintes:
• O algoritmo de Needleman-Wunsch - produz um alinhamento global ótimo entre
duas seqüências biológicas, permitindo lacunas (gaps). Esse algoritmo constrói
progressivamente um alinhamento ótimo usando soluções prévias de
alinhamentos ótimos de subseqüências menores (Needleeman e Wunsch, 1970) e
(Gotoh, 1982).
• O algoritmo de Smith-Waterman - produz um alinhamento local ótimo entre
duas seqüências biológicas. A versão original é devida a (Smith e Waterman,
1981) e a eficiente versão affine gap scores que é normalmente usada é devida a
Gotoh (1982).
• Os HMMs lineares e os Perfis-HMMs.
Os algoritmos que empregam a abordagem de PD são usados em Bioinformática para
alinhamento de pares de seqüências ou de múltiplas seqüências entre si (perfil), para
alinhamento de uma ou múltiplas seqüências a um perfil-HMM ou para treinamento de
um perfil-HMM. Esses algoritmos podem ser utilizados, com diversos critérios de
otimização (funções de custo), para encontrar um alinhamento ótimo (de custo mínimo
ou de score máximo) ou calcular outras medidas de interesse. Em biologia molecular as
funções de custo típicas incluem, em ordem crescente de complexidade:
1) custos simples, onde cada custo de mutação é uma constante;
80
2) custos lineares de lacunas (gaps) e custos lineares de lacunas afins, onde
inclusões e remoções são orçadas através de uma função linear;
3) custos lineares por partes de lacunas e custos côncavos de lacunas.
Muitas vezes o algoritmo desenvolvido por métodos de PD reduz drasticamente a
complexidade, quando comparado com a complexidade do algoritmo direto, ou seja,
para um algoritmo direto de complexidade exponencial às vezes pode se construir um
algoritmo de PD com complexidade polinomial. Outras vezes o algoritmo de PD tem a
mesma complexidade do algoritmo direto mas com coeficientes de ordem mais baixos
(Toscani e Veloso, 2001).
Tomando-se como base a metodologia padrão de análise de desempenho de algoritmos,
tem-se que a complexidade de espaço e de tempo dos algoritmos de PD usados para a
computação de medidas de interesse nos HMMs são, respectivamente, de ordem O(n2)
bytes e O(n3) operações, considerando-se que as strings e o HMM são de comprimentos
similares, digamos ~n, ou tomando-se o comprimento da maior string. Mas,
alternativamente, pode-se considerar a um tempo unitário para efetuar os cálculos de
uma célula de PD e, então, sob essa ótica a complexidade de tempo seria de ordem
O(n2) unidades de tempo. Nesse trabalho foi adotada essa ótica alternativa para
expressar a complexidade de tempo, principalmente para facilitar as comparações com
os resultados obtidos em Tarnas e Hughey (1998).
81
82
CAPÍTULO 3
ALGORITMOS DE PROGRAMAÇÃO DINÂMICA COM CHECKPOINTS
USADOS EM PERFIS-HMMS
3.1 Introdução
Comparação e alinhamento de seqüências são procedimentos computacionalmente
dispendiosos, comumente utilizados em bioinformática, que freqüentemente recorrem
aos já clássicos métodos de PD com requerimentos de memória de trabalho da ordem de
O(n2). Esses algoritmos podem se beneficiar de técnicas ou métodos de economia de
espaço (memória de trabalho). Essa economia de espaço, geralmente, é obtida em troca
de uma queda de desempenho no tempo de execução do algoritmo. Os HMMs lineares e
mais precisamente os perfis-HMMs, que são os modelos alvos deste estudo, utilizam
métodos de PD para o cálculo de medidas de desempenho e para o treinamento (cálculo
dos parâmetros) do modelo.
Nesse capítulo é apresentada uma família de métodos, que fazem uso do método de PD
e do paradigma (ou estratégia) de checkpoints para comparação e alinhamento de
seqüências, com economia de espaço, que podem ser usados com o algoritmo de Viterbi
na computação do melhor caminho, com o algoritmo forward (ou backward) na
computação da ( )λ|OP e com o algoritmo de Baum-Welch (forward-backward) no
treinamento de HMMs, especialmente nos perfis-HMMs. Esses métodos são
convenientes para implementação tanto serial quanto paralela. Numa implementação
serial eles requerem uso de memória da ordem de O( L nm ) com um fator de queda de
desempenho proporcional a L, onde m é o comprimento da maior seqüência, n o
comprimento da menor seqüência (ou do modelo perfil-HMM) e L é um inteiro
arbitrário indicando o nível do algoritmo, que pode ser determinado considerando-se a
quantidade de memória disponível e os comprimentos m e n. Numa implementação
paralela, com processadores paralelos conectados linearmente ou em malha, com O(m)
elementos de processamento, eles requerem espaço da ordem de O( L n ) por elemento
83
de processamento e têm complexidade de tempo da ordem de O(mn) com um fator de
queda de desempenho proporcional a L.
Os algoritmos de PD com checkpoints são fundamentalmente idênticos ao algoritmo de
PD básico ou padrão. A principal diferença é que os algoritmos de PD com checkpoints
armazenam, na primeira fase de computação para frente, apenas parte da matriz de PD;
armazenam apenas algumas linhas (colunas ou diagonais) convenientemente
selecionadas da matriz de PD, que são denominadas checkpoints. As seções ausentes,
aquelas entre as linhas (colunas ou diagonais) de checkpoints, são re-computadas
quando necessário durante a fase de retrocedimento.
As demais seções deste capítulo tratam dos algoritmos de PD com economia de espaço,
em particular, da conceituação básica dos algoritmos de PD com checkpoints. Na
segunda seção é apresentada uma revisão da literatura referente aos algoritmos de PD
com economia de espaço atualmente utilizados em Bioinformática, especialmente na
classe de perfis-HMMs. Essa revisão tem o objetivo de situar o problema e analisar o
estado da arte destes algoritmos e é centrada, principalmente, nos tópicos de interesse
deste trabalho. A terceira seção trata do estado da arte dos algoritmos de programação
dinâmica que fazem uso do paradigma de checkpoints.
3.2 Revisão da Literatura
Myers (1991) em um relatório técnico, faz uma revisão da técnica de programação
dinâmica usada em algoritmos de comparação de seqüências em Biologia Molecular.
Segundo ele o paradigma de programação dinâmica, que é um paradigma
computacional de grande e variada aplicabilidade, já tinha se tornado, nesta época, uma
técnica clássica e bem estabelecida em Biologia Computacional. Ele examina variações
do problema básico, que são motivadas, especialmente, por aplicações em Biologia
Computacional, entretanto ele não trata da aplicação destes algoritmos de PD aos
HMMs.
Segundo Myers (1991) a comparação de bio-seqüências possibilita aos Biólogos
moleculares verificar, a partir de similaridades encontradas entre essas, se as
84
características físicas ou funcionais de uma das seqüências são, também, verdadeiras
para a sua análoga. Tais comparações, geralmente, constituem-se no alinhamento das
seqüências, ou de seções destas, de tal forma que as similaridades entre elas sejam
evidenciadas. Essas comparações podem ser efetuadas do ponto de vista de
maximização da similaridade entre as seqüências ou de minimização da distância entre
elas. Na verdade, esses dois problemas não são contraditórios mas duais do ponto de
vista de modelos de otimização. A maximização da similaridade pode ser visto, de
forma intuitiva, como a busca por alinhamentos que possuem poucas lacunas e cujos
símbolos alinhados sejam idênticos ou tenham funções muito similares. Na
minimização da distância entre as seqüências o problema é encarado do ponto de vista
da transformação da seqüência A na seqüência B através de um número mínimo de
operações “evolucionárias”. Nessa visão, um símbolo alinhado de A é substituído pelo
seu complementar em B, um símbolo não alinhado em A é removido e um símbolo não
alinhado em B é adicionado. Adicionalmente, a noção de otimização requer algum
critério de otimização ou de atribuição de escores e tradicionalmente é fornecida uma
função, freqüentemente associada com a plausibilidade biológica do alinhamento, que
atribui um valor real a cada possível par alinhado (Myers, 1991).
O problema de maximização da similaridade pode ser formalizado, de maneira intuitiva
e esclarecedora, através de um grafo de edição, um grafo direcionado com arestas
rotuladas, que no jargão de ciência da computação é um autômato finito. A cada aresta
rotulada deste grafo de edição pode ser associado um peso ou custo, de forma que a
soma dos pesos das arestas rotuladas de um caminho seja exatamente a soma dos
escores dos pares alinhados no correspondente alinhamento. Desta forma o problema se
reduz ao de computar o caminho de peso-mínimo através do grafo de edição, desde o nó
de origem até o nó de encerramento (sink). No jargão de ciência da computação, a
comparação de seqüências tem sido reduzido ao problema de caminhos mínimos sobre
um grafo com estrutura bastante regular (para mais detalhes veja Myers (1991)).
Segundo Myers (1991) os problemas de comparação de seqüências surgiram mais cedo
no campo de Ciência da Computação do que no campo da Biologia Molecular, numa
formulação para instâncias mais simples do problema. O problema de encontrar a maior
85
subseqüência comum – LCS – e seu dual, o problema de determinar o script de edição
mínimo, surgiram no contexto de aplicações tais como comparar o conteúdo de arquivos
de computadores e corrigir a ortografia de palavras. Aqui é importante notar que estas
aplicaçõs de Ciência da Computação constituem instâncias do problema que requerem
apenas funções de custos, subjacentes, de natureza bastante simples, com custos
discretos e inteiros, o que permite um desempenho algorítmico muito bom. Esse
desempenho algorítmico não pode ser obtido para as aplicações de Biologia Molecular,
pois essas requerem funções de custos de natureza bem mais complexas.
Nesse formalismo de PD, se o interesse reside somente no custo de um alinhamento
ótimo, então é necessário guardar somente a linha anterior da matriz de PD para
computar a linha corrente, graças à natureza das dependências de dados da recorrência
de PD, requerendo espaço da ordem de ( )( )nmO ,min . Mas se é necessário recuperar o
alinhamento ótimo, o caminho de estados que produziu tal alinhamento, então é
necessário guardar toda a matriz de PD, requerendo espaço da ordem de O .
Entretanto, apesar da complexidade deste problema de PD ser polinomial de grau 2,
quando computando o alinhamento de seqüências muito grandes, o que é muito
freqüente quando alinhando bio-seqüências, o requerimento de espaço pode ser um fator
limitante, mais do que o requerimento de tempo, pois pode-se estar fora da memória
disponível no computador real. Desta forma, a construção de algoritmos com
requerimentos de espaço reduzido é muito importante para a comparação de bio-
seqüências (Myers, 1991).
( )mn
Usando esse algoritmo como um sub-procedimento que produz somente o custo de um
alinhamento ótimo, Hirschberg (1975) apresenta um algoritmo D&C que determina o
alinhamento ótimo, requerendo espaço da ordem de ( )nmO + . Entretanto, esse
algoritmo é apresentado num contexto mais simples, na determinação da LCS entre
duas seqüências. Mas, Myers e Miller (1988) mostraram que esse algoritmo D&C de
Hirschberg pode ser aplicado a muitos algoritmos de comparação de seqüências, que
admitem versões de espaço linear para computar somente o custo do alinhamento ótimo
entre duas seqüências.
86
O algoritmo apresentado por Hirschberg (1975) adota a estratégia de D&C para
computar a LCS entre duas seqüências, e requer somente espaço linear, ao custo de uma
queda de desempenho dada por um fator de 2 em relação ao tempo quadrático do
algoritmo de PD padrão. Dadas duas strings A e B, o algoritmo de Hirschberg divide,
por exemplo, a string B ao meio - quer dizer para a coluna média da matriz de PD - e
encontra em que lugar nessa coluna média o alinhamento ótimo cruza. Restando, desta
forma, dois problemas menores de alinhamento, correspondente às duas regiões da
matriz de PD divididas pela coluna média e pela linha que contém o ponto de
cruzamento do alinhamento ótimo na coluna média. Esses dois problemas menores são,
então, resolvidos recursivamente da mesma forma. Os pontos de cruzamento do
alinhamento ótimo na coluna média destes sucessivos particionamentos são
determinados executando-se o algoritmo de PD para frente na primeira metade da
matriz de PD, ou nas sucessivas seções desta matriz de PD, e em ordem reversa na outra
metade.
Hunt e Szymanski (1977) apresentaram um algoritmo para o problema LCS entre duas
seqüências com complexidade de tempo da ordem de ( )nrO loglog , onde r é o número
de pontos concordantes entre as duas seqüências. Entretanto, o parâmetro r pode ser de
tamanho mn, quando as seqüências são proximamente relacionadas, ou seja, é
potencialmente de complexidade ( )mnΩ . Desta forma, apesar de não poder melhorar a
complexidade deste algoritmo em termos dos parâmetros de entrada m e n, ele é
bastante efetivo para aquelas aplicações que são bastante esparsas com relação ao
parâmetro r, como aquelas de comparação de arquivos de computadores. Entretanto,
Hunt e Szymanski (1977) não apresentaram nenhuma consideração, quanto à
possibilidade de estender essa técnica para que fosse utilizada com outras funções de
custo, que possibilitassem comparações mais efetivas, ou para que fosse utilizada em
aplicações mais complexas.
Em (Myers, 1986) foi apresentado um algoritmo para o problema do script de edição
mínimo, que apresenta complexidade de tempo da ordem de ( )dnO , onde d é número
de indels (número de adições e de remoções) e de substituições no alinhamento original.
87
Esse algoritmo é dito ser sensível à saída, pois ele depende do parâmetro d, que
caracteriza o valor do escore da comparação das duas seqüências. Pode ser verificado
que esse algoritmo apresenta um comportamento de pior caso para seqüências
totalmente divergentes, com d sendo da ordem de ( )nmO + , e um comportamento de
melhor caso para seqüências totalmente similares, com d sendo da ordem de , onde
k é um inteiro. Podendo, desta forma, não apresentar queda na complexidade de tempo
em termos dos parâmentros m e n. Apesar disso, esse algoritmo é muito simples e sua
complexidade de tempo esperada é da ordem de
( )kO
( )2dn +O . Adicionalmente, Myers
(1986) apresentou uma variação deste algoritmo com complexidade de tempo, para o
pior caso de execução, da ordem de ( )2log dnn +O , usando como sub-procedimentos
complexas árvores de sufixos e algoritmos que requerem tempo constante para a
determinação do ancestral comum mínimo.
O algoritmo de Myers (1986) e o de Hunt e Szymanski (1977) não podem ser
estendidos, a princípio, para utilização com outras funções de custo mais complexas,
que possibilitem comparações mais efetivas, ou para utilização em aplicações mais
complexas. Constituindo, portanto, para o problema mais geral de comparação de
seqüências, em alguns resultados algorítmicos pontencialmente úteis.
Segundo Myers e Miller (1988) o método apresentado por Hirschberg (1975) para a
computação da LCS é uma abordagem mais geral, que pode ser aplicada a qualquer
algoritmo de comparação de seqüências cuja versão para determinar somente o custo do
alinhamento ótimo requer apenas espaço linear. Sendo, portanto, superior, teoricamente
e em aplicações práticas, às recentes estratégias propostas para economia de espaço e,
merecendo maior visibilidade no seio da comunidade científica. Desta forma eles
desenvolveram, a partir das idéias de Hirschberg, uma versão de espaço linear do
algoritmo apresentado por Gotoh (1982), que admite funções de custo afim –
penalidades afim de lacunas (affine gap penalities).
O algoritmo desenvolvido por Gotoh (1982) possui complexidades de tempo e espaço
da ordem de para comparação de duas seqüências, produzindo como saída tanto (mnO )
88
o custo mínimo quanto o alinhamento ótimo dos símbolos das duas seqüências. Esse
algoritmo admite uma implementação com espaço linear, se somente o custo do
alinhamento ótimo é requerido. Desta forma usando esse procedimento para determinar
somente o custo de um alinhamento ótimo e o método apresentado por Hirschberg
(1975), Myers e Miller (1988) desenvolveram a versão linear do algoritmo de Gotoh
(1982) ou, alternativamente, estenderam o algoritmo D&C de Hirschberg para admitir
uma função de custo afim – com penalidades de lacunas dadas por uma função afim.
Rick (2000) apresenta uma versão de espaço linear para o algoritmo desenvolvido por
Rick (1995) para determinar o comprimento da LCS e produzir como saída a própria
LCS. A complexidade de tempo destes algoritmos é da ordem de
((( pnppmnsO )))−+ ,min , onde s é o tamanho do alfabeto e p é o tamanho da LCS.
Rick (1995) observa que os algoritmos desenvolvidos até aquela época para o problema
da LCS não apresentam ganho de desempenho com relação a complexidade de tempo
da ordem de da abordagem de PD, mas alguns destes apresentam performance
bem melhor para certas classes de seqüências. Esses algoritmos freqüentemente são
sensíveis a parâmetros de saída, tais como p, d e s. Alguns são convenientes para tratar
classes de pares de seqüências onde a LCS é muito grande, em média, outros para
pequenas LCS, em média. Mas essa decisão nem sempre é facil, pois freqüentemente se
conhece pouco da natureza das seqüências a serem comparadas ou a classe de
seqüências é heterogênea. Além disso, quando as seqüências de uma classe têm
aproximadamente o mesmo tamanho e são formadas por símbolos de um alfabeto
pequeno, tomados de maneira mais ou menos regular, o tamanho da LCS pode variar
entre
(mnO )
m31 e m3
2 , em média, dependendo dos tamanhos do alfabeto (no caso de
tamanhos: 4, 12, 14). Essa é uma situação típica da análise de bio-seqüências em
Biologia Molecular e não têm sido dada atenção específica a esses problemas. O
algoritmo proposto por Rick (1995 e 2000) apresenta boa peformance para todas essas
classes de problemas, tanto no nível teórico quanto prático, sendo, portanto, pouco
sensível aos parâmetros de saída . Veja na Tabela 3.1, adaptada de Rick (2000), um
resumo do desempenho destes algoritmos de espaço linear para o problema da LCS.
89
Crochemore et al (2003) apresentaram dois algoritmos para determinar o tamanho e o
próprio alinhamento da LCS. Esses algoritmos são um melhoramento, baseado em
paralelismo de bits, dos algoritmos de Hirschberg (1975), baseado na técnica D&C, e de
Hunt e Szymanski (1977), baseado na computação eficiente de todos os pontos
concordantes dominantes (contornos). O paralelismo de bits já tem sido bastante usado
para encontrar o tamanho, p, da LCS, mas eles estendem o uso desta idéia para a
recuperação da LCS. Os algoritmos que recorrem a esse paralelismo ao nível de palavra,
que “empacotam” bits numa palavra de computador, são denominados algoritmos de
vetores de bit (bit-vector algorithms).
TABELA 3.1- Resumo do Desempenho de Alguns Algoritmos de Espaço Linear para o Problema LCS.
Ano AUTOR TEMPO FQD PARADIGMA
1975 Hirschberg ( )mnO 2 PD
1986 Myers ( )( )pnnO − 2 Caminhos Mínimos
1990 Wu et al ( )( )pmnO − 2 Caminhos Mínimos
2000 Goeman et al ( )( )pnpmmpmO −+log,min [5,25; log m] Contornos
2000 Rick ( )( )pnppmO −,min 2 Contornos
FQD: Fator de queda de desempenho para o pior caso de desempenho do algoritmo. FONTE: Adaptada de RICK (2000).
O algoritmo de Hunt e Szymanski (1977) tem complexidade de espaço da ordem de
, com pior caso de complexidade de tempo da ordem de ( )+ nrO nnO loglog2 . É
importante salientar que nesta mesma época Hirschberg (1977) também apresentou um
algoritmo baseado nessa técnica de contornos com complexidade de tempo da ordem de
, com pior caso ( )pnO nO . Esses algoritmos são eficientes para aplicações onde r é
pequeno, ou seja, eles são dependentes do parâmetro r.
( )
( )2
Talvez o grande ganho dos algoritmos de vetores de bits (bit-vector algorithms)
apresentados por Crochemore et al (2003) seja a baixa sensibilidade aos parâmetros de
entrada, s e w, ou aos parâmetros de saída, p e r. Eles asseguram que esses
melhoramentos são de interesse prático e superam os algoritmos básicos em
90
desempenho. O algoritmo de Hirschberg (1975) melhorado com a adição do paralelismo
de bits mantém os requerimentos de espaço ao nível linear e obtém um ganho de
desempenho ao nível da complexidade de tempo, que agora é da ordem de ( wmn )O . Já o
algoritmo de Hunt e Szymanski (1977) mantém a complexidade linear de espaço e
obtém um pequeno ganho de desempenho com relação ao tempo de execução,
apresentando agora complexidade da ordem de ( )( )nnq logO + , pois rq , mas com
um pior caso da mesma ordem do algoritmo original, que é de
≤
( )nlogn log2O .
Grice et al. (1995 e 1997) apresentaram uma família de métodos, denominados
algoritmos de L-níveis com checkpoints, para computar alinhamentos de seqüências,
que são convenientes tanto para implementação serial quanto paralela. Numa
implementação serial eles requerem uso de memória da ordem de O( L nm ) com um
desempenho da ordem de O( ), com um fator de queda de desempenho de L. Numa
implementação paralela, com O(m) processadores paralelos conectados linearmente ou
em malha, eles requerem espaço da ordem de O(
mn
L n ) por elemento de processamento
com um fator de queda de desempenho de L.
Esses algoritmos são fundamentalmente idênticos aos algoritmos de PD padrões e
podem ser usados com as diversas funções de custo, que freqüentemente são usadas na
biologia computacional. Eles podem ser utilizados para o treinamento de perfis-HMMs,
através do algoritmo de Baum-Welch (forward-backward). Na determinação do melhor
alinhamento entre duas seqüências, um dos membros desta família de algoritmos, o de
L-níveis com checkpoints por diagonais com retrocedimento restrito, supera em
desempenho, numa análise comparativa a posteriori, o algoritmo de Hirschberg baseado
na estratégia D&C e iguala o requerimento de espaço linear deste.
Os algoritmos de PD de L-níveis com checkpoints caracterizam-se por calcularem a
matriz de PD completa, por exemplo, numa execução do algoritmo forward, mas
somente armazenando determinadas linhas, colunas ou diagonais da matriz de PD a
intervalos regulares (checkpoints), para que numa fase de retrocedimento as seções entre
dois destes checkpoints possam ser recalculadas sem a necessidade de recalcular a
91
matriz de PD desde o início. Isto permite reduzir o requerimento de memória da ordem
de O(nm) para O( L nm ). O algoritmo de PD padrão é considerado o algoritmo de 1-
nível com checkpoints.
Em Grice et al. (1997) foram implementados o algoritmo de 2-níveis com checkpoints
por linhas, o de 2-níveis com checkpoints por diagonais, o algoritmo D&C de
Hirschberg e o algoritmo básico de PD. Os algoritmos de 2-níveis com checkpoints
foram implementados com e sem retrocedimento restrito. Os experimentos seriais foram
efetuados numa estação, não-carregada, Sun UltraSparc, modelo 140 com 64 Mb de
memória, e os experimentos paralelos num computador paralelo Maspar MP-2204 com
4096 elementos de processamento e 64 Kb de memória por elemento de processamento.
Os experimentos seriais para encontrar o melhor alinhamento entre duas seqüências
mostraram que a queda de desempenho em tempo total de execução são proximamente
relacionados para seqüências de até 4.000 resíduos. Mas para seqüências maiores os
algoritmos com economia de espaço mostraram-se muito superiores em desempenho.
Os algoritmos de 2-níveis com checkpoints, sem retrocedimento restrito, apresentaram
uma queda de desempenho dada por um fator ~2 em relação ao algoritmo básico para
seqüências de até 4.000 resíduos e uma aceleração no desempenho por um fator de 10
ou mais, para seqüências entre 4.000 e 7.000 resíduos.
Observa-se que o algoritmo de 2-níveis, com checkpoints por diagonais e
retrocedimento restrito, mostrou-se mais rápido em relação ao tempo total de execução
que o algoritmo básico para seqüências maiores do 1.000 resíduos, e sempre foi mais
rápido que os demais algoritmos para todos os comprimentos de seqüências.
Nesses experimentos a queda de desempenho em relação ao tempo de CPU foram
proximamente relacionadas para todos os tamanhos de seqüências no intervalo
considerado. Somente o algoritmo de 2-níveis com checkpoints por diagonais e
retrocedimento restrito apresentou desempenho superior ao algoritmo de PD básico para
todas as seqüencias maiores do que 500 resíduos, no intervalo considerado. Todos os
outros algoritmos quando comparados ao algoritmo de PD básico apresentaram o
92
melhor desempenho para seqüências com 7.000 resíduos, mas mesmo assim com uma
queda no desempenho. Os 2 algoritmos com 2-níveis de checkpoints sem
retrocedimento restrito apresentaram uma queda no desempenho por um fator de no
mínimo 1,5. O D&C apresentou uma queda no desempenho por um fator ~1,25. O
algoritmo com 2-níveis de checkpoints por linhas e retrocedimento restrito apresentou
uma queda no desempenho por um fator ~1,1.
Tarnas e Hughey (1998) discutiram as implementações e performance dos algoritmos de
alinhamento de seqüências com uso de espaço reduzido baseado em checkpoints,
apresentados em Grice et al. (1995 e 1997), e a inclusão da versão por diagonais destes
algoritmos de 2-níveis de checkpoints no pacote de modelagem com HMMs
denominado SAM (Sequence Alignment and Modeling) do Departamento de Engenharia
de Computação da Escola de Engenharia Jack Baskin da Universidade da Califórnia em
Santa Cruz. Implementaram o código e avaliaram a sua performance numa estação DEC
Alpha 255 com um clock de 233 MHz, 96 Mb de memória principal e um cache L2 de
1 Mb. Efetuaram vários experimentos usando uma quantidade de memória variável,
com o propósito de verificar o nível de eficiência quando o tamanho do problema cresce
além do tamanho do cache L2, e para verificar o nível de eficiência do algoritmo de PD
padrão quando o tamanho do problema cresce além do tamanho da memória principal.
Mas, segundo eles, essa verificação não pôde ser efetuada pois o código é
computacionalmente limitado devido à manipulação de log-probabilidades. Nesses
experimentos foram calculados os tempos de CPU e os tempos totais de execução para
os algoritmos de Viterbi e forward-backward padrões e para os de 2-níveis com
checkpoints por diagonais.
Foram realizados dois experimentos. No primeiro experimento foi considerado um
HMM com 500 nós e seqüências de proteínas com comprimentos variando de dezenas
a 10.000 resíduos. O desempenho do algoritmo forward-backward com checkpoint é
cerca de 10% mais lento que o algoritmo padrão. Já o algoritmo de Viterbi padrão e o
com checkpoint tiveram desempenho semelhante até o início de paginação. Pode ser
notado, comparando-se os gráficos que sumarizam os tempos de CPU e os tempos de
execução, que para seqüências acima de 8.000 resíduos os algoritmos padrões causam
93
paginação excessiva na estação de trabalho com 96 Mb de memória, o que torna
inviável a obtenção de resultados para seqüências acima deste tamanho. No segundo
experimento foi considerado um HMM com 2.000 nós e seqüências de RNAs com
comprimentos variando de dezenas a 10.000 resíduos. Os resultados foram similares ao
do primeiro experimento, mas a degradação da memória virtual teve início para
seqüências de cerca de 2.000 resíduos, devido ao tamanho do modelo, o que torna
inviável a obtenção de resultados para seqüências acima deste tamanho.
Da análise dos resultados destes experimentos pôde ser observado que o algoritmo de 2-
níveis com checkpoints apesar de efetuar muito mais computação que o algoritmo
padrão, tem uma queda de desempenho muito pequena, de somente 10%, em relação ao
algoritmo básico. Isto pode ser devido a utilização eficiente do sistema de memória,
particularmente no uso da cache.
Tarnas e Hughey (1998) observaram, com base nesses experimentos e na experiência
anterior com os algoritmos PD de 2-níveis com checkpoints por diagonais, que a
complexidade das condições de contorno adicionadas quando processando diagonais
não proporcionou um retorno equivalente no ganho de performance. E, com base nos
resultados destes experimentos, eles planejaram reimplementar o loop interno do pacote
SAM com checkpoints por linhas. É importante salientar que a complexidade das
condições de contorno foi agravada pelo uso da técnica de retrocedimento restrito. Eles
observaram, também, que o algoritmo de PD de 2-níveis com checkpoints tem
requerimento de memória, O( nm ), maior do que o da abordagem D&C de
Hirschberg, O( ), mas possui três vantagens que faz dele uma forte alternativa a
abordagem D&C de Hirschberg. Primeiro, ele pode ser usado para efetuar cálculos
forward-backward. Segundo, a codificação dele é mais simples que a da abordagem
D&C, principalmente para checkpoints por linhas ou colunas. Por último, a abordagem
D&C, que é completamente recursiva, parece ter um custo constante extra (overhead)
maior do que aquele da abordagem com checkpoints, que não é totalmente recursiva.
nm +
Além disso, eles relataram que a computação da matriz de PD ao longo das diagonais
remove todas as dependências de dados do loop interno do algoritmo de PD, liberando o
94
compilador para realizar uma otimização mais efetiva e uma reorganização do código
dentro do loop interno. Isto torna essa família de algoritmos de 2-níveis com
checkpoints por diagonais apropriada para implementação em códigos para
processamento paralelo e processamento vetorial.
Powell et al. (1999) aplicaram a técnica de checkpoints ao algoritmo de Hirschberg,
apresentado acima, e ao algoritmo rápido de Ukkonen, na versão apresentada por
(Myers, 1986), para obter um alinhamento entre duas strings. A alternativa ao
algoritmo de Hirschberg, com a técnica de checkpoints, pode ser aplicada a muitas
outras funções de custo, além da função de custo simples ou distância de edição básica.
Essa alternativa mantém o requerimento de espaço linear do algoritmo original de
Hirschberg com uma queda de desempenho por um fator constante de cerca de 2,5.
Entretanto não foi apresentada nenhuma comparação com o algoritmo de PD básico.
Uma versão do algoritmo de Ukkonen apresentada por Myers (1986) é cerca de 42%
mais rápido que o algoritmo básico de PD, quando efetuando o alinhamento de duas
seqüências. Esse algoritmo possui uma complexidade de tempo média da ordem de
O( ), onde d é o custo de edição entre duas strings (seqüências). Se for necessário
determinar somente o custo de edição, o espaço requerido é da ordem de O(d), mas se
for necessário também o alinhamento, o espaço requerido é da ordem de O(d
2dn +
2). A
alternativa apresentada em Powell et al. (1999), com a técnica de checkpoints, reduz o
requerimento de espaço do algoritmo original de Ukkonen para O(d), mesmo quando
produzindo um alinhamento, com uma queda de desempenho por um fator constante de
cerca de 3,5. Entretanto não foi apresentada nenhuma comparação com o algoritmo de
PD básico e nem com o algoritmo de Hirschberg com checkpoints.
O algoritmo de Ukkonen (Myers, 1986), quando aplicado ao alinhamento de duas
seqüências, acelera o desempenho do algoritmo de PD básico, com base em
determinados características da matriz de PD: nem todas as entradas da matriz de PD
são necessárias; as diagonais são monotônicas e não-decrescentes; e somente o ponto
final de uma série de combinações (matches) é importante. E com base nesses fatos ou
características uma matriz alternativa U é construída, onde cada entrada U[ab, d] = i
95
contém a distância máxima, que pode ser obtida, ao longo da seqüência A, na diagonal
com um custo jiab −= [ ]jiPDd ,= . Uma linha da matriz U corresponde a uma
diagonal da matriz de PD e uma coluna de U corresponde a um contorno (contour) de
custo fixo na matriz PD.
Os algoritmos alternativos apresentados em Powell et al. (1999) podem ser acelerados
aumentando-se o uso de memória (usando-se mais checkpoints) e são apropriados para
determinar o melhor alinhamento entre duas seqüências. É importante salientar que os
dados usados como amostras nos experimentos efetuados por eles, foram gerados
aleatoriamente com um alfabeto composto de 26 símbolos. Primeiro a string A foi
gerada aleatoriamente e, então, a string B foi gerada de A com probabilidades fixas de
0,2 para mudança, 0,1 para inserção e 0,1 para remoção (foram permitidas discordâncias
para o mesmo caractere).
Wheeler e Hughey (2000) exploraram o paradigma de checkpoints, especialmente a
disposição dos checkpoints com a intenção de melhorar o desempenho dos algoritmos
de PD de L-níveis com checkpoints. Eles analisaram e procuraram determinar a
disposição ótima dos checkpoints na matriz de PD que produzisse uma melhor
performance nos algoritmos de PD de L-níveis com checkpoints. Nesse sentido eles
consideraram os custos das diferentes estratégias de disposição dos checkpoints, que
podem ser determinados em termos do número de células de PD calculadas. Eles
observaram que até onde os requerimentos de espaço são suficientemente baixos, para
evitar o risco de troca excessiva de páginas entre a memória virtual e o disco, o cálculo
de células de PD domina a computação e, portanto, a diferença entre esses esquemas de
disposição dos checkpoints na matriz de PD. Isto se deve ao fato que esses algoritmos
de PD com checkpoints caminham de maneira regular através de sucessivas células da
matriz de PD e, desta forma segundo eles, o uso amplo e eficiente da memória cache é
feito através de longas linhas, sendo assim a diferença entre ajustar na memória
principal e ajustar na cache pode ser insignificante.
96
O algoritmo de PD de L-níveis com checkpoints pode calcular uma matriz de PD com
um número máximo de linhas dado por: , desde que
, e para n
−+== ∑
=− L
LRirRr
R
iLL
1)()(
11
( ) RRr =1 )(RrL= o algoritmo utiliza todo o espaço reservado de memória, R,
com uma disposição ótima dos checkpoints. Entretanto, para não é
possível calcular a matriz de PD com o algoritmo de nível L-1 e há um desperdício de
espaço reservado e perda de performance quando se usa um algoritmo de nível L. Essa
perda de performance por um fator de
)(RrL)(1 nRrL <<−
( )1~ −LL se deve principalmente a uma má
disposição dos checkpoints.
Wheeler e Hughey (2000) apresentaram para r )()(1 RrnR LL <<− um algoritmo
denominado algoritmo de ( -níveis com checkpoints, que pode melhorar muito a
situação de perda de performance. Entretanto os algoritmos de
)LL ,1−
( )L,1L − -níveis com
checkpoints não são ótimos. O algoritmo ótimo transita livremente entre níveis para
fazer uso ótimo da memória disponível para qualquer subproblema. Para estudar a
disposição ótima dos checkpoints Wheeler e Hughey (2000) utilizaram um algoritmo de
programação dinâmica de ordem O(n3).
Assintoticamente, o algoritmo de L-níveis calcula Ln linhas, o algoritmo de ( )-
níveis calcula pelo menos
LL ,1−
( ) 11 +− nL linhas e o algoritmo ótimo, de ( ) -
níveis, calcula pelo menos ( )
K,2, −L1, −LL
( ) 12 −− L nL1 −− nL linhas. Na prática, para valores de pior
caso, um dado R e , o algoritmo de 150≥n ( )L,1L − -níveis é cerca de 10% mais lento
que o algoritmo ótimo.
A abordagem de Wheeler e Hughey (2000) para melhorar a performance do algoritmo
de PD com checkpoints por diagonais foi similar àquela usada no algoritmo com
checkpoints por linhas. Foi utilizada programação dinâmica para encontrar o algoritmo
com checkpoints ótimo e, então, tentou-se descobrir um algoritmo mais simples, mas
ainda útil para uso geral. No primeiro conjunto de experimentos, foram realizados o
alinhamento de pares de seqüências. Quando as seqüências eram aproximadamente
97
iguais em tamanho, o algoritmo melhorado, com checkpoints por diagonais, efetuou um
pouco menos cálculos de células de PD que o algoritmo básico D&C, com duas linhas
de memória cada. Contudo, para seqüências de tamanhos diferentes, o algoritmo D&C
foi claramente superior. Mas, quando mais que duas linhas de memória estão
disponíveis, o algoritmo com checkpoints por diagonais explora melhor esses recursos
extra, e desempenha significativamente melhor que o D&C. Já o algoritmo de Powell
efetua até 2,5 % menos cálculos de células que o algoritmo melhorado com checkpoints
por diagonais, mesmo considerando uma variação de recursos extra de memória.
O pacote de modelagem HMM denominado SAM foi modificado para o segundo
conjunto de experimentos relatados em Wheeler e Hughey (2000), o algoritmo de PD de
2-níveis com checkpoints por linhas foi substituído pelo algoritmo de (1,2)-níveis com
checkpoints por linhas. O pacote modificado foi testado contra o original numa estação
Sun UltraSpark I com 64 MB de memória. Esses algoritmos permitiram a avaliação do
melhor caminho simples de estados para uma seqüência de 80.000 resíduos contra um
HMM com 80.000 nós, mas a avaliação de todos os caminhos não foi possível para
esses tamanhos de seqüências e HMMs. Eles realizaram experimentos, variando os
tamanhos da memória de PD numa faixa de até 60 Mb. Na avaliação de todos os
caminhos o algoritmo melhorado foi 3 a 12% mais rápido, independente do tamanho do
HMM, e o melhoramento típico foi cerca de 10% quando alocados 10 a 20 Mb de
memória para o problema. Para um HMM com 5.000 nós, os dois algoritmos iniciaram
paginação para uma alocação de 60 Mb de memória, com uma queda de desempenho
por um fator de 1,9.
Wheeler e Hughey (2000) apresentaram as seguintes conclusões: quando é requerido
somente o melhor caminho de estados, o algoritmo de Powell é uma escolha apropriada
como uma alternativa ao algoritmo básico D&C e ao algoritmo com checkpoints por
diagonais; mas quando é necessário avaliar todos os caminhos, a única escolha são os
algoritmos com checkpoints.
A seguir são apresentados os resultados obtidos em três teses relacionadas com o tema
em estudo, devidas a Comet (1998), a Pedersen (2000) e a Birney (2000). Essas teses
98
tratam de algoritmos em Biologia Computacional para a análise e comparação de
seqüências biológicas. Esses estudos, também, recorrem a métodos e técnicas de
programação dinâmica mas focalizam outros aspectos destes algoritmos, diferentes
daqueles que são objeto deste estudo. Essa diferença de foco ficará clara nas descrições
destes trabalhos realizadas a seguir.
Comet (1998) em sua tese de doutorado trata dos algoritmos de comparação de
seqüências biológicas, no campo de aplicação da biologia molecular. Ele desenvolve um
estudo para expressar os algoritmos de programação dinâmica, usados para alinhamento
global e local de seqüências biológicas, através da álgebra (max, +), o que possibilita
relacionar o problema com problemas de outros campos de aplicação e lançar mão de
experiência e conhecimentos acumulados no estudo destes problemas, nessas áreas.
Essa abordagem permite construir um autômato, que permite a busca em um banco de
dados em tempos lineares, embora a dimensão deste autômato seja muito grande.
Entretanto, mesmo que a construção deste autômato seja inviável por causa de sua
dimensão, segundo ele, essa construção permite a ligação entre o método de
alinhamento de seqüências usando programação dinâmica e os métodos de combinação
de padrões (pattern-matching) com erros.
Nesse estudo é proposto um algoritmo (ou método) de alinhamento, que leva em conta
outras informações além daquelas relativas à estrutura primária da seqüência biológica,
com o propósito de melhorar os alinhamentos obtidos por programação dinâmica,
obtendo alinhamentos que são biologicamente mais significativos. No algoritmo são
considerados os padrões contidos no banco de dados PROSITE, favorecendo um
alinhamento se ele coloca em correspondência as ocorrências do mesmo padrão
presente nas duas seqüências. Adicionalmente, é feito um estudo da significância do
escore de um alinhamento e propõe-se o Z-escore como um bom guia estatístico para a
avaliação da significância biológica de um alinhamento, pois esse parece ser um pouco
mais confiável que o próprio escore.
O trabalho de pesquisa desenvolvido por Pedersen (2000) e relatado em sua tese de
doutorado se refere à construção de algoritmos que tratam problemas da área biológica e
99
apresenta soluções com relevância biológica. Numa primeira parte ele faz uma revisão
do campo de biologia computacional focando na análise e predição de estruturas de
seqüências biológicas. No capítulo 2 ele revisa métodos para comparação de seqüências
biológicas, concentrando-se na comparação de duas seqüências biológicas para
determinar sua proximidade ou distância evolucionária, e em métodos para comparação
de famílias inteiras de seqüências biológicas, o que envolve o problema mais abstrato de
comparar dois HMMs. No capítulo 3 ele revisa métodos para encontrar regularidades
em strings, que podem ser aplicados na análise de seqüências (strings) biológicas,
concentrando-se nos métodos para encontrar repetições em cadeia (tandem repeats),
pares máximos e sub-strings quase-periódicas máximas. No capítulo 4 ele revisa
métodos para predição de estruturas biológicas, levando em conta elementos na
predição total da estrutura tridimensional da biomolécula a partir de sua descrição
enquanto uma seqüência (string) biológica, concentrando-se em métodos para a
predição da estrutura secundária de seqüências de RNA e da estrutura terciária de
proteínas.
A segunda parte da tese de Pedersen (2000) consiste da reimpressão de 6 artigos já
publicados em congressos ou jornais especializados ou através de relatórios técnicos,
que apresentam os resultados do trabalho de pesquisa feito durante a sua participação no
programa de doutoramento em Ciência da Computação na Faculdade de Ciência da
Universidade de Aarhus, na Dinamarca. Esses artigos apresentam resultados no campo
de biologia computacional concentrando-se nos resultados obtidos na análise e predição
de estruturas de seqüências biológicas, referentes àqueles problemas revisados na
primeira parte da tese. Não estenderemos a revisão do trabalho de pesquisa de Pedersen
(2000), para abranger os resultados apresentados nesses artigos, por estarem fora do
foco principal deste trabalho.
A tese de Birney (2000) está voltada para a construção de ferramentas ou de recursos de
programação,. Ele trata, principalmente, da construção de um kit de ferramentas de
programação que fazem uso de PD para analisar seqüências biológicas e extrair
informação útil. Esse kit de ferramentas é denominado Dynamite. Na construção de
cada ferramenta ele primeiro trata da teoria e da experiência acumulada na utilização
100
destas e depois das questões programáticas que asseguram a eficiência e a construção de
aplicações úteis. Para as ferramentas que produzem resultados biológicos, ele apresenta
uma avaliação de sua eficácia. Ele também apresenta uma discussão do sucesso ou falha
de cada ferramenta construída.
Um kit de ferramentas como o Dynamite, proposto por Birney (2000), permite que o
pesquisador do campo da Bioinformática se concentre mais nos problemas biológicos
reais e na sua modelagem do que na implementação destes modelos, pois a
implementação dos métodos no campo da PD, na maioria das vezes, requer muito
tempo para a programação e depuração do código. Ele escolheu construir o seu kit de
ferramentas baseado em um compilador ao invés de baseado em bibliotecas de código
reutilizável, pois essa abordagem permite migrar para uma descrição de mais alto nível
do problema de PD. Essa abordagem permite, também, maior flexibilidade, separando o
compilador, as bibliotecas de rotinas e de suporte e o código de controle. Uma vantagem
adicional de se usar uma linguagem compilada é que essa permite tirar vantagem de
características específicas do hardware que será utilizado, podendo ser independente do
hardware se baseado em padrão ANSI.
As diferentes implementações possibilitadas pelo Kit Dynamite fornecem processos de
cálculo que são baseados na mesma definição de máquina de estado finito (autômato).
Além do que o Dynamite analisa completamente a definição da máquina de estado
finito numa definição que totalmente descreve a máquina, de forma que arquiteturas
bastante complicadas de máquinas alvo podem ser consideradas. Esse Kit proposto por
(Birney, 2000) fornece as seguintes implementações:
• Escore do alinhamento de duas seqüências (strings) através do algoritmo
forward ou do algoritmo de Viterbi, ambos com requerimento de espaço linear;
• Alinhamento Viterbi de duas seqüências com requerimento de espaço quadrático
e linear;
101
• Alinhamento recursivo de duas seqüências com requerimento de espaço linear,
que faz uso da técnica D&C introduzida na comparação de seqüências por
Hirschberg (1975);
• Busca serial ou paralela (através de pthreads) em banco de dados de seqüências;
• Geração de código conveniente para hardware especializado, tal como para a
OneModel API (OneModel XL/G port).
Outro foco importante da tese de Birney (2000) está na avaliação da eficácia das
ferramentas construídas com o Kit Dynamite, que produzam resultados com
significância biológica. Para tanto ele se concentra no algoritmo GeneWise, que
compara informações de proteínas diretamente com a seqüência de DNA genotípica
(genômica), com a predição de gene ocorrendo simultaneamente à comparação de
proteínas. A eficácia do algoritmo GeneWise foi testada na análise de famílias de
proteínas (perfis-HMMs) do banco de dados Pfam, na comparação de genes da minhoca
contra o banco de dados PfamI, na comparação de genes do cromossomo humano nº 22
e na densidade de codificação de Humanos x C.elegans.
3.3 Algoritmos de Programação Dinâmica com Checkpoints
Até onde tem conhecimento o paradigma de checkpoints foi apresentado, para uso em
conjunto com o algoritmo de PD padrão em implementações paralelas, por Grice et al.
(1995). Ele foi considerado para implementações seriais por Grice et al. (1997),
estendido para uso com perfis-HMMs por Tanas e Hughey (1998) e melhorado por
Wheeler e Hughey (2000). Desta forma toda essa seção baseia-se nos artigos de: Grice
et al. (1995), Grice et al. (1997), Tarnas e Hughey (1998) e Wheeler e Hughey (2000).
Os algoritmos de PD baseados nesse paradigma são fundamentalmente idênticos aos
algoritmos de PD padrões e podem ser usados com as diversas funções de custo, que
freqüentemente são usadas na biologia computacional.
Antes de prosseguir na descrição e definição dos algoritmos baseados no paradigma de
checkpoints é necessário definir alguns parâmetros. Já foi definido anteriormente, que m
102
é o tamanho da maior seqüência e n é o tamanho da menor seqüência (ou do perfil-
HMM), mas para simplificar a discussão pode ser assumido sem perda de generalidade
que m = n. Seja mMEMR = o número de linhas (colunas ou diagonais) da matriz de
PD que podem ser armazenadas, onde MEM é o número de células da matriz de PD que
podem ser armazenadas na memória disponível.
O algoritmo de PD padrão pode computar, usando como memória somente R = 2 linhas
da matriz de PD, o escore de um alinhamento ótimo ou a medida P(O|λ) – a
probabilidade de ocorrência da seqüência de observações O, dado o modelo λ –,
possuindo nesse caso complexidade de espaço da ordem de ( )nO sem nenhuma perda
de desempenho. Mas, quando é necessário utilizar um procedimento de retrocedimento
parcial para recuperar o alinhamento ótimo ou um retrocedimento completo – sobre
todos os caminhos ou alinhamentos – o algoritmo de PD básico tem complexidade de
espaço da ordem de ( ) ( )2nOmn =O , com a suposição de que nm = . O algoritmo de PD
com retrocedimento parcial precisa calcular pelo menos nmmn ++ células da matriz de
PD e tem complexidades de tempo da ordem de ( ) ( )2mn nO=O , enquanto que o
algoritmo com retrocedimento completo precisa calcular pelo menos células e
tem complexidade de tempo da ordem de
mnmn +
( ) ( )2nOmnO = .
Quando é necessário executar um procedimento de retrocedimento, a abordagem de PD
padrão cria uma matriz de PD, uma tabela m x n, para armazenar valores na memória.
Mas, infelizmente, na maioria das vezes essa tabela não pode ser ajustada
completamente na memória do PC ou da workstation, quando o modelo e os tamanhos
das seqüências são grandes. A solução é usar um método que requeira menos espaço nas
computações da matriz de PD.
Essa limitação do uso computacional do algoritmo de PD básico, quando associado a
um procedimento de retrocedimento, devido à complexidade de espaço da ordem de
O(n2) pode ser superada tirando vantagem do fato que não é necessário salvar o
resultado de todas as comparações para efetuar o retrocedimento. Ela pode ser superada
com checkpoints e re-computação; é necessário salvar somente informação do estado da
103
computação (checkpoints) que seja suficiente para re-computar eficientemente as
comparações quando elas forem requeridas. Esses checkpoints de informação do estado
da computação incluem todos os custos totais requeridos para recalcular futuras linhas,
colunas ou diagonais das seções entre dois checkpoints consecutivos. Essas linhas,
colunas ou diagonais de checkpoints podem ser salvos de forma regular e apropriada
para que o retrocedimento através da matriz de PD possa ser efetuado em espaço
limitado, ou seja, sem a necessidade de armazenar em memória toda a matriz de PD.
Esses algoritmos são denominados algoritmos de PD de L-níveis de checkpoints e
caracterizam-se por calcularem a matriz de PD completa, por exemplo, numa execução
do algoritmo forward, mas somente armazenando determinadas linhas, colunas ou
diagonais da matriz de PD a intervalos convenientes e regulares (checkpoints), para que
numa fase de retrocedimento, as seções entre dois destes checkpoints possam ser
recalculadas sem a necessidade de recalcular a matriz de PD desde o início. Isto permite
reduzir assintoticamente o requerimento de memória da ordem de ( )nmO para O( L nm )
ao preço de uma queda de desempenho por um fator de 1≤0 , < kkL , onde L é um
inteiro tal que . nL ≤<0
Quando a memória disponível mMEMR = (linhas) for insuficiente para armazenar a
matriz de PD completa, para que se possa executar um procedimento de retrocedimento
sobre a matriz, a memória pode ser dividida em dois segmentos: um segmento
denominado Rcp para armazenar os checkpoints e um segmento denominado Rtb usado
para o cálculo de seções da matriz de PD na fase de computação para frente (forward) e
na fase de retrocedimento. Frequentemente, a memória disponível R (linhas) pode ser
otimizada fazendo com que 2RRtbcpR == linhas, mas às vezes essa divisão pode ser
móvel ou ter outra configuração que seja mais conveniente.
O algoritmo de PD padrão é considerado o algoritmo de 1-nível com checkpoints, já
descrito nesse trabalho. A seguir será feita uma descrição dos algoritmos de níveis mais
altos, iniciando com o de 2-níveis de checkpoints, que é o mais fácil de ser descrito.
Para facilidade e simplicidade de tratamento e raciocínio é assumido em quase todo o
104
restante desta seção 3.3, que os checkpoints são por linhas. As mesmas análises e
tratamentos podem ser simplesmente transpostos para checkpoints por colunas e por
diagonais.
3.3.1 Algoritmo A: 2-Níveis com Partição Fixa
Para o algoritmo com 2-níveis de checkpoints com partição fixa de memória o uso de R
linhas de memória disponível pode ser otimizado fazendo-se 2RRR tbcp == .
Como o procedimento de retrocedimento só pode ser iniciado após o cálculo do último
valor (última célula) da matriz de PD, o valor da célula PDn,m, então os resultados da
computação da primeira seção da matriz de PD, Rtb, podem ser descartados, com
exceção da última linha, pois eles podem ser recalculados das condições iniciais. A
última linha precisa ser armazenada (salva) como um checkpoint. As próximas Rtb
linhas são computadas (calculadas) e a última linha deste segmento deve ser salva como
um outro checkpoint, descartando-se as demais linhas, pois elas podem ser re-
computadas do primeiro checkpoint salvo. Esse processo é repetido até que a última
linha da matriz de PD seja calculada. A última seção da matriz de PD, a que contém a
última linha, não é descartada, ela é usada para iniciar o processo de retrocedimento
sobre a matriz.
O processo de retrocedimento inicia-se na última seção da matriz de PD que foi
computada e salva. Os resultados da seção anterior são, então, re-computados a partir do
penúltimo checkpoint salvo e o procedimento de retrocedimento é, então, executado
nessa seção. Re-computa-se, então, os resultados da seção anterior a partir do
checkpoint prévio e executa-se o procedimento de retrocedimento nessa seção. Esse
processo é repetido até que a primeira seção da matriz de PD seja alcançada e o
procedimento de retrocedimento executado nessa primeira seção, completando-se,
portanto, o retrocedimento sobre a matriz de PD.
A análise da performance deste algoritmo é simples de ser feita. Em primeiro lugar,
considerando a suposição assumida de que se nm = , então o maior número de linhas
que o algoritmo de 2-níveis com partição fixa pode calcular com R linhas é
105
( )1+= cptb RRn linhas. Entretanto supondo o particionamento ótimo 2RRR tbcp == ,
então o número de linhas da matriz de PD que pode ser computado com R checkpoints é
dado por ( )24
122
12 RRRRRR cptb +=
+=+=n . Desta forma, tem-se que
nRn 2≤⇒RRn24
2
≤⇒≥ , de onde pode-se concluir que o algoritmo de 2-níveis
com partição fixa tem complexidade de espaço da ordem de ( )nnO .
n2
Com relação à complexidade de tempo do algoritmo de 2-níveis com partição fixa tem-
se que são calculadas n2 células da matriz de PD numa execução forward do algoritmo
de PD, mais nn2− células no processo de retrocedimento, mais 2n ou n2 células,
dependendo se o retrocedimento é parcial ou completo. Desta forma tem-se que para o
retrocedimento parcial são computadas nnnnnnnn 22222 222 +−=+−+n células
da matriz de PD, contra do algoritmo básico, e para o retrocedimento completo nn 22 +
nnnnnn 32 222 =+−+ nn 22 − células, contra 2 do algoritmo básico. Portanto,
considerando que se gaste um tempo unitário para calcular cada célula da matriz de PD,
tem-se que o algoritmo de 2-níveis com partição fixa tem complexidade de tempo da
ordem de
2n
( )2nO com queda de desempenho de nnn 22 − , ou seja, da ordem de ( )2nO .
O fator de queda de desempenho é dado por 10 ,2 ≤< kk .
3.3.2 Algoritmo B: 2-Níveis com Partição Móvel
O algoritmo de 2-níveis com partição fixa de memória não usa de forma eficiente as R
linhas de memória disponível. A divisão entre memória de armazenagem de
checkpoints, Rcp, e memória de trabalho, Rtb, pode ser móvel. Na primeira fase de
computação da matriz de PD para frente as linhas de memória Rcp que não estão
armazenando checkpoints podem ser usadas para o cálculo de seções da matriz de PD,
como memória de trabalho, Rtb. Então, quando é necessária mais memória Rcp a divisão
move na outra direção aumentando a memória Rcp e diminuindo a memória Rtb. Na fase
de retrocedimento o processo de movimento de memória é invertido; a divisão de
106
memória retrocede diminuindo a memória Rcp após os checkpoints terem sido usados e
aumentando a memória Rtb.
Numa primeira etapa, o algoritmo de 2-níveis com partição móvel de memória calcula
as primeiras R linhas da matriz de PD, mas somente a R-ésima linha é salva e as demais
são descartadas. Numa segunda etapa o algoritmo calcula as próximas R-1 linhas da
matriz de PD, mas somente a (R-1)-ésima linha é salva e as demais são descartadas.
Esse procedimento é repetido até que a linha final seja calculada e armazenada, e a fase
de retrocedimento pode ser, então, iniciada. Durante a fase de retrocedimento seções da
matriz de PD são recalculadas para frente, desde o checkpoint prévio. O retrocedimento
continua através de cada seção até que o checkpoint para aquela seção seja alcançado e
as informações desejadas são, então, armazenadas. Remove-se a seção atual da memória
e a próxima seção é, então, recalculada usando agora a nova quantidade de memória
disponível.
A análise de desempenho deste algoritmo é análoga ao algoritmo de 2-níveis com
partição fixa de memória. O algoritmo de 2-níveis com partição móvel de memória pode
computar com R linhas de memória disponível, uma matriz de PD de dimensões ,
onde o número n de linhas é dado por (Grice, 1997):
nn×
( )22
1 2
1
RRRRinR
i
+=
+== ∑
=
(3.1)
Da equação (3.1) tem-se que: nnRR 222
2
≤≤⇒≥n . Logo pode-se concluir que o
algoritmo de 2-níveis com partição móvel tem complexidade de espaço da ordem de
( )nnO , entretanto faz uso mais efetivo de sua memória disponível R.
Com relação à complexidade de tempo do algoritmo de 2-níveis com partição móvel,
tem-se que: são calculadas n2 células da matriz de PD numa execução forward do
algoritmo de PD, mais nnn −2 células no processo de retrocedimento, mais 2n ou n2
células, dependendo se o retrocedimento é parcial ou completo. Desta forma tem-se que
107
para o retrocedimento parcial são computadas nnnnnnnnn 222 222 +−=+−+
células da matriz de PD, contra do algoritmo básico, e para o retrocedimento
completo
nn 22 +
nnnnnn =+−+ 222 nn −23 células, contra do algoritmo básico.
Portanto, considerando que se gaste um tempo unitário para calcular cada célula da
matriz de PD, tem-se que o algoritmo de 2-níveis com partição móvel tem
complexidade de tempo da ordem de
22n
( )2nO com queda de desempenho de nnn −2 ,
ou seja, da ordem de ( )2nO . O fator de queda de desempenho é dado por . 10 ≤< k ,2k
)(6
2 R +
1ii
R
i∑
=
3.3.3 Algoritmo C: 3-Níveis com Partição Móvel
Uma versão 3-níveis do algoritmo B pode reduzir o uso de memória ainda mais, ou seja,
com a mesma quantidade de memória usada pelo algoritmo B pode-se calcular matrizes
de PD ainda maiores com uma versão de 3-níveis de checkpoints com partição móvel de
memória. Esse algoritmo é um algoritmo de 3-níveis de checkpoints: computação e
armazenagem de checkpoints de nível-3; computação e armazenagem provisória de
checkpoints de nível-2 (algoritmo B); e a computação e armazenagem provisória de
checkpoints de nível-1 (algoritmo de PD básico). O algoritmo C chama inicialmente o
algoritmo B para calcular um número de linhas suficiente para encher a memória
disponível R e armazena a última linha como o primeiro checkpoint de nível-3. Então,
de forma similar ao algoritmo B, após armazenar um checkpoint de nível-3, o algoritmo
B é chamado para calcular tantas linhas quanto forem possíveis antes do próximo
checkpoint de nível-3 ser armazenado.
Quando o algoritmo C está enchendo a memória de checkpoints, ele chama o algoritmo
B com uma variação de espaços de R até 1. Desta forma o número máximo de linhas
que o algoritmo C pode computar com R linhas de checkpoints é dado por:
( ) ( ) ( )1121
21
1
RRiinR
i
+=+=
+= ∑
=
(3.2)
108
Da equação (3.2) tem-se que: 333
366
nnRR≤≤⇒≥n . Logo, pode-se concluir que o
algoritmo de 3-níveis com partição móvel tem complexidade de espaço da ordem de
( )3 nnO , fazendo uso mais efetivo de sua memória disponível R.
Quanto à complexidade de tempo do algoritmo de 3-níveis com partição móvel, tem-se
que: são calculadas n2 células da matriz de PD numa execução forward do algoritmo de
PD, mais 322 nnn − células no processo de retrocedimento, e mais 2n ou n2 células,
dependendo se o retrocedimento é parcial ou completo. Desta forma tem-se que para o
retrocedimento parcial são computadas nnnnnnnnn 2322 32322 +−=+−+ células
da matriz de PD, contra do algoritmo básico, e para o retrocedimento completo nn 22 +
32 nnn −2322 42 nnnnn =+−+ células, contra 2 do algoritmo básico. Portanto,
considerando que se gaste um tempo unitário para calcular cada célula da matriz de PD,
tem-se que o algoritmo de 3-níveis com partição móvel tem complexidade de tempo da
ordem de
2n
( )2nO com queda de desempenho de 32 n−2 nn , ou seja, da ordem de ( )2nO .
O fator de queda de desempenho é dado por 3 10 , ≤< kk .
3.3.4 Algoritmo D: L-Níveis com Partição Móvel
Nessa seção considera-se um algoritmo de L-níveis com particionamento de memória
móvel, ou seja, um algoritmo com nível de recursão arbitrário e particionamento móvel
de memória. O número máximo de linhas, ( )RrL , que podem ser computadas com o
algoritmo de L-níveis com partição móvel de memória para uma determinada
quantidade de memória disponível de R checkpoints é dado por:
( ) (∑=
−=R
iLL irRr
11 ) . (3.3)
Assumindo que r , essa recorrência pode ser resolvido por: ( ) RR =1
109
( ) ( )( )
( ) ( ) ( )( ) 12...1
1...21!!1!11
×××−××+××−+×−+
=−
−+=
−+=
LLRRLRLR
LRLR
LLR
RrL . (3.4)
Da equação (3.4) pode-se derivar, de forma geral, a seguinte desigualdade:
( ) L
LL
L LR
LRRr ≥≥
!. Logo pode-se concluir que o número de linhas que podem ser
calculadas pelo algoritmo de L-níveis com partição móvel de memória é da ordem de
( )LL LRO linhas, com complexidade assintótica de tempo da ordem de O . Com a
suposição de que e tomando o número de checkpoints
( mrL )nm = nrL = , tem-se que a
complexidade assintótica de tempo é da ordem de ( )2nO e a de espaço é da ordem de
( )L nnO . A queda de desempenho, que se paga em troca da redução do espaço de
memória é dado por . 1≤< k0 ,kL
Mas se for considerado valores pequenos para L, tem-se da equação (3.4), geralmente,
que: ( ) L
LL
L LR
LRRr ≥≥
!. Produzindo um comportamento assintótico de tempo ( )2nO
com uma queda de desempenho proporcional a L e um comportamento assintótico de
requerimento de espaço ( )L nnO .
De forma semelhante pode-se derivar da equação (3.4), para R pequeno, a seguinte
desigualdade: ( ) R
RR
L RL
RLRr ≥≥
!. Ou seja, para R pequeno tem-se, geralmente, que:
( ) ( )RRL RLORr = . Logo, existe um algoritmo com requerimento de espaço O e
com uma penalidade na performance de
(Rn)
( )R nO .
Assim, a equação (3.4) para fornece um quadro de pontos de desenho de projeto
no tradeoff tempo-espaço. Embora seja possível explorar várias alternativas no tradeoff
espaço-tempo, geralmente a melhor escolha é usar toda a memória disponível e
encontrar o nível mais baixo de checkpoints que pode ser usado para retroceder sobre
uma matriz de PD de dimensões n
( )RrL
nnm ×=× , ou seja, que acomoda o tamanho da
maior seqüência, de tamanho n. Desta forma, o nível ótimo é dado por:
110
−+≤=
iiR
niL1
:min . (3.5)
Usando um método de busca exponencial, L pode ser encontrado em tempo
, para maiores detalhes veja em (Wheeler e Hughey, 2000). ( nO loglog )
Para um dos extremos tem-se com L = 1 o algoritmo de PD básico, que requer R = n
linhas de checkpoints para computar e armazenar a matriz de PD. Esse algoritmo de PD
padrão tem complexidade assintótica de espaço da ordem de ( )2nO e de tempo da
ordem de ( )2nO .
No outro extremo tem-se L = n, que requer um número mínimo de checkpoints (R = 2
linhas) para computar durante a fase de retrocedimento n seções da matriz de PD,
repetindo a computação para frente (forward) da primeira célula da matriz de PD, a
célula PD0,0, até cada uma das linhas. Quer dizer, uma matriz de dimensões arbitrárias
com linhas pode ser computada com apenas 2 linhas de memória e, em
seguida pode ser efetuado um procedimento de retrocedimento sobre a matriz com essas
2 linhas de memória. Assim, tem-se células na fase para frente mais
células na fase de retrocedimento parcial ou na fase de retrocedimento
completo, produzindo um total de ou células, respectivamente. Esse
algoritmo de PD com n-níveis de checkpoints tem, portanto, complexidade assintótica
de espaço da ordem de e complexidade de tempo limitada por
( ) 12 += nrn
mn2
n2+
nmn 22 +22 nmn +
232 nn +n2 3
( )nO ( ) ( )3nO=2mnO ,
com um coeficiente igual a 2. Essa instância pode ser tomada para a estimativa de pior
caso.
Se forem usados níveis de checkpoints, o algoritmo terá complexidade de tempo nblog
( )2nO , com queda de desempenho dada por um fator de e complexidade de
espaço .
nblog~
( )nnO blog
111
3.3.5 Algoritmo E: Viterbi com L-Níveis de Checkpoints por Diagonais com
Retrocedimento Restrito
Se o interesse reside somente no melhor caminho através da matriz de PD a
performance do algoritmo de PD com checkpoints e partição móvel de memória pode
ser melhorada assumindo checkpoints por diagonais e aplicando a esse o principio
D&C, pois o melhor caminho passa, somente, através de uma região limitada da matriz
de PD entre duas diagonais quaisquer. Esse algoritmo gasta menos tempo e memória
para computar o melhor caminho e é referido na literatura como algoritmo de PD de L-
níveis por diagonais com retrocedimento restrito. O retrocedimento restrito também
pode ser considerado nas versões por linhas e por colunas.
O uso de checkpoints por diagonais, ao invés de por linhas ou colunas, acarreta duas
ineficiências com relação ao requerimento de espaço, a saber. A primeira refere-se à
necessidade de um número maior de posições de checkpoints, pois existem nm +
diagonais, ao invés de m linhas ou n colunas. A segunda refere-se à quantidade de
informações do estado da computação que devem ser armazenados como checkpoints,
pois a computação de uma diagonal requer valores das duas diagonais anteriores, logo
para cada checkpoint devem ser armazenadas 2 diagonais da matriz de PD. Mesmo
assim, esse algoritmo mantém o comportamento assintótico de espaço e de tempo da
versão por linha (Grice et al., 1997).
O tempo para calcular o melhor caminho através da matriz de PD é a soma do tempo na
primeira fase de computações para frente, na computação da matriz de PD, mais o
tempo gasto na segunda fase de computações para traz, no retrocedimento. Usando-se, o
paradigma de checkpoints, com checkpoints por diagonais, gasta-se a princípio um
tempo n2 na primeira fase. Mas, na segunda fase pode-se economizar tempo de re-
computação aplicando o princípio D&C, pois não é necessário re-computar toda a
subseção entre dois checkpoints consecutivos.
Supondo-se em primeiro lugar que exista uma relação um a um entre os pontos de uma
grade ou treliça e as células da matriz de PD, em segundo lugar que o espaçamento ou
112
distância entre dois checkpoints, por diagonais, sucessivos seja c, e finalmente, em
terceiro lugar se o melhor caminho passa através do ponto ( )ji, pertencente ao
checkpoint k, na diagonal jidk += , então o melhor caminho precisa alcançar a
diagonal entre os pontos cjidk −+=−1 ( )cji −, e ( )jci ,− .
Desta forma, é necessário re-computar, usando o algoritmo de nível L-1, somente uma
região triangular limitada por , ( )ji, ( )cji −, e ( )jci ,− , entre os checkpoints dk e dk-1,
contendo 22c células da matriz de PD. Assim, seguindo o desenvolvimento
apresentado em Grice et al. (1997), então existem LLL nnnm 22 ≤=+ regiões
contendo 22c células da matriz de PD, que necessitam ser re-computadas. É necessário
observar que no algoritmo com partição fixa de memória, LL
Ln
nnc
1
22 −
== para todas as
regiões, e no com partição móvel de memória, c varia de 1 até LL
n1−
, sucessivamente,
para cada região. Assumindo-se, então, partição fixa de memória e considerando que
para um nível arbitrário L , existem L n2 checkpoints com um espaçamento entre eles
de LL
n1−
, então na fase de retrocedimento são re-computadas L n2 seções de dimensões
×
−−L
LL
L
nn11
21
L
da matriz de PD usando o algoritmo de nível L-1. Logo, a estimativa do
tempo total, T , gasto pelo algoritmo com L-níveis de checkpoints por diagonais e
retrocedimento restrito para calcular o melhor caminho pode ser determinado em termos
de complexidade assintótica conforme mostra a desigualdade (3.6), dada a seguir.
( )n
( )
+≤
+≤
−
−
−
−L
L
LLL
L
LL
L nTnnnTnnnT1
12
1
12
212 (3.6)
A partir da equação (3.6) pode-se mostrar por indução que o tempo total, T , gasto
pelo algoritmo com L-níveis de checkpoints por diagonais e retrocedimento restrito para
( )nL
113
calcular o melhor caminho tem complexidade assintótica ( )2nO , quando ,
onde . Veja a prova nas equações a seguir.
nL log≤
nn bloglog =
( )( ) ( )
( )
( )( )
( )
log
log
2
2
b
T
nT
n
T
nO
n
n
∴
−
3
nblog
( ) ( )( ) ( )
( ) ( ) ( ) ( )
( ) ( ) ( )( ) ( ) ( )
( ) ( ) ( )( ) ( )
( ) ( )2log
12
1log2
1loglogn2
log
221log
52
452
5
24
23
424
222
22
32
222
12
2
21
21
bn
quemostrar se-deve , que se-Supondo
log1log
57
58
59
54
46
47
43
35
32
32
31
32
21
21
21
nOnT
nnbnTnnTnnT
nOnOnT
OnTnnnnnnTnnnT
nOnTnnnnnTnnnT
nOnnnnnnnnnTnnnT
nTnnnnnnTnnnTnOnTnnT
n
nnn
n
nn
=∴
+≤
+=
+≤
==
=∴++++=+≤
=∴+++=+≤
=++=
++=+≤
=∴+=×+=+≤=∴=
−−
−−
M
3
(3.7)
Pode-se perceber da equação em (3.6) que, numa análise a priori, assintoticamente não
existe penalidade de tempo, para níveis de checkpoints L ≤ , quando calculando o
melhor caminho, usando retrocedimento restrito. Entretanto essa análise descarta os
coeficientes multiplicativos e quando se deseja comparar algoritmos da mesma ordem
de complexidade é necessário elucidar a dimensão desta penalidade, levando-se em
consideração esses coeficientes. Desta forma pode-se estabelecer um fator de queda de
desempenho com base nesses coeficientes.
Pode ser deduzido das 5 primeiras equações em (3.7) que a desigualdade em (3.6) tem
equação de recorrência, dada a seguir em (3.8), composta de L fatores no lado direito,
que incluem necessariamente o primeiro e o último fator.
( ) nnnnnnT LL
LL
LL
L +++++≤+−− 222122 L (3.8)
Fazendo em (3.8) e considerando logaritmos de base 2, bastante usados em
biologia computacional, e logaritmos de base 10 e 100, obtém-se as desigualdades em
(3.9), dada a seguir.
nL blog=
114
( )( ) ( ) ( ) ( ) ( )
( )
( )
( ) nnnnnnnnT
nnnnnnnnT
nnnnnnnnT
nnnnnnnnT
LLn
LLn
LLn
nnb
nbnb
nbnb
nbnb
nbnb
nb
b
+++++++≤
+++++++≤
+++++++≤
+++++++≤
−−
−−
−−
++−−−
2
2
3
2
3
2
2
222
log
2
2
3
2
3
2
2
222
log
2
2
3
2
3
2
2
222
log
2log
100100100100100
:se- tem100, base de logaritmos se-doConsideran1010101010
:se- tem10, base de logaritmos se-doConsideran22222
:se- tem2, base de logaritmos se-doConsideran
100
10
2
log2log
log3log
log3log2
log2log2
log1log2
L
L
L
L
(3.9)
Se for considerado em (3.9) somente a equação para logaritmos de base 2, pode ser
mostrado que a soma dos coeficientes dos termos de segundo grau, n , tende a 2
quando n cresce. De onde se pode concluir que a queda de desempenho para o nível
, quando calculando o melhor caminho com o algoritmo de PD com L-níveis
de checkpoints por diagonais e retrocedimento restrito, tem penalidade de tempo dada
por um fator ~2 vezes.
2
nL 2log=
Se for considerado em (3.9) somente a equação para logaritmos de base 10, pode ser
mostrado que a soma dos coeficientes dos termos de segundo grau, , tende a
1,111111... quando n cresce. De onde se pode concluir que a queda de desempenho para
o nível , quando calculando o melhor caminho com o algoritmo de PD com
L-níveis de checkpoints por diagonais e retrocedimento restrito, tem penalidade de
tempo dada por um fator ~1,111111... vezes.
2n
nL 10log=
Se for considerado em (3.9) somente a equação para logaritmos de base 100, pode ser
mostrado que a soma dos coeficientes dos termos de segundo grau, , tende a
1,0111111... quando n cresce. De onde se pode concluir que a queda de desempenho
para o nível , quando calculando o melhor caminho com o algoritmo de PD
com L-níveis de checkpoints por diagonais e retrocedimento restrito, tem penalidade de
tempo dada por um fator ~1,0111111... vezes.
2n
nL 100log=
Para seqüências de tamanhos arbitrários, especialmente para n grande, pode ser
observado, com relação ao nível de checkpoint , que nL bnbloglog =
115
nnn LLL210100 logloglog <<
Rlog
, o que implica em requerimentos de memória para esses níveis
de checkpoints . Por exemplo, para n , tem-se nnn RR210100 loglog >> 000.10=
( ) ( ) ( )1442log2
10100 loglog =< nL=<= nn LL e ( ) ( ) ( )7200210100 loglog =25 log>=>= nnn RR
2log=
n
( )nblog
R
L
L blog≤
.
nL 2log=
,log≤ nL b ,100 com ≥b
Desta forma pode ser notado das equações em (3.7), (3.8) e (3.9) e das observações
feitas nos parágrafos anteriores que os níveis de checkpoints dados por
constituem instâncias do problema que podem ser uma estimativa razoável para o
comportamento assintótico de pior caso destes algoritmos de PD com L-níveis de
checkpoints por diagonais e retrocedimento restrito dados por . Pois
fornece níveis de checkpoints muito altos contra requisições de memória
muito baixas. Por outro lado, se forem considerados níveis de checkpoints dados por
algoritmos de bases grandes (≥100), podem ser obtidas instâncias do problema que
constituem uma estimativa razoável do comportamento assintótico de melhor caso, pois
quando obtém-se níveis de checkpoints muito baixos contra
requisições de memória muito altas.
n
Isto está de acordo com o comportamento desta classe de algoritmos com checkpoints,
que é: aumentando-se a memória e diminuindo-se o nível provoca-se uma aceleração no
desempenho destes algoritmos com checkpoints; pode-se trocar espaço por ganho de
desempenho e vice-versa. Pode ser notado das equações em (3.7) e (3.9) que a queda de
desempenho aumenta proporcionalmente ao nível L (diretamente) e à quantidade de
memória R (inversamente).
Do exposto nos parágrafos anteriores pode-se concluir que o fator de queda de
desempenho pode ser de até 2 vezes, para o algoritmo de checkpoints por diagonais com
retrocedimento restrito (D&C), quando o número de níveis for da ordem O e
desde que haja memória suficiente para ser utilizada para esse nível e tamanho da matriz
de PD. O que se não contraria, pelo menos ameniza ou coloca em cheque a conclusão
relatada em Grice et al. (1997, p. 51), também baseada na equação (3.6) através de uma
prova por indução, que não está documentada, mas ao que tudo indica semelhante
116
àquela em (3.7): “Thus when looking for the single best path, there is no time penalty
with the checkpoint method when the number of levels is ( )nO log ”. Deve ser ressaltado
que está conclusão foi assumida nos trabalhos posteriores relatados por Tarnas e
Hughey (1998) e Wheeler e Hughey (2000), que integram o estado da arte com respeito
a essa família de algoritmos de PD com L-níveis de checkpoints.
) 2n=
)
O algoritmo com L-níveis de checkpoints por diagonais com retrocedimento restrito
para calcular o melhor caminho apresenta comportamento de requerimento de espaço
semelhante aos demais membros desta família de algoritmos. Seja o espaço
necessário para armazenar checkpoints de nível L suficientes para que se faça o
retrocedimento sobre a matriz de PD contendo aproximadamente 2n diagonais. O
requerimento de espaço do algoritmo de nível 1é
( )nSL
(1 nS . O algoritmo de nível 2
necessita de espaço para nn8 locações de memória e espaço para computar um bloco
nn × usando o nível imediatamente mais baixo: ( ( )n =nS = 82
∴ ( ) ( )nnOnS =2 . O algoritmo de nível 3 necessita de espaço para 3 nn12 locações de
memória e espaço para computar um bloco 3 n3 usando o nível imediatamente
mais baixo:
n ×
( ) ( )32
33 12 nSnnnS += ∴ ( ) ( )3
3 nnOnS = . Isto pode ser escrito como
uma recorrência dada pela desigualdade a seguir.
nnnSnn ++ 81
( ) ( )LL
nSnLnnS LL
L
1
14−
−+≤ (3.10)
Da recorrência dada pela desigualdade (3.10) pode ser mostrado por indução para
que
1≥L
( ) ( )LL nnOnS = .
Apesar de tudo, o algoritmo de L-níveis para calcular o melhor caminho apresenta
redução no requerimento de espaço quando se toma nL blog= níveis. Nesse caso tem-
se que ( ) ( )nnOn bn loglog =S e não, necessariamente, de O(n) como o apresentado em
Grice et al. (1997).
117
Segundo Grice et al. (1997), esse algoritmo de PD com nL log= níveis de checkpoints
por diagonais, combinado com o princípio D&C, iguala a performance assintótica do
algoritmo de Hirschberg, de requerimento de espaço de ( )nO e de tempo de execução
de ( )2nO , para calcular o melhor caminho através da matriz de PD. Mas como foi visto
nessa seção os requerimentos de espaço são maiores, são da ordem de O . ( )nn blog
3.3.6 Disposição Ótima de Checkpoints por Linhas (Colunas) e o Algoritmo
Melhorado
O algoritmo de PD de L-níveis com checkpoints pode calcular uma matriz de PD com
um número máximo de linhas dado por: , desde que
. Para
−+== ∑
=− L
LRirRr
R
iLL
1)()(
11
( ) RRr =1 )(Rrn L= o algoritmo utiliza todo o espaço reservado de memória, R,
com uma disposição ótima dos checkpoints. Entretanto, para não é
possível calcular a matriz de PD com o algoritmo de nível L-1 e há um desperdício de
espaço reservado e perda de performance quando se usa um algoritmo de nível L. A
maior ineficiência do algoritmo original de nível L acontece quando
)(RrL
(1
)(1 nRrL <<−
1) +=n −rL R .
Essa descontinuidade ou perda de performance por um fator de (~ −LL )1 se deve
principalmente a uma má disposição dos checkpoints.
A solução proposta por Wheeler e Hughey (2000) explora a disposição ou localização
dos checkpoints como a solução para essa perda de performance por um fator de
( 1~ −LL ) . Os algoritmos de PD padrão e aqueles que fazem uso do paradigma de
checkpoints usam de forma eficiente a memória cache, pois eles caminham de maneira
regular através de sucessivas células da matriz de PD de tal forma, que os elementos
das linhas de cache sejam usados de maneira regular e sucessiva. Portanto a diferença
entre ajustar na memória e ajustar na cache pode ser insignificante.
Logo, o custo de estratégias diferentes de disposição dos checkpoints pode ser dado em
termos do número de células da matriz de PD que necessitam ser computadas ou re-
computadas. A computação de células da matriz de PD domina as diferenças entre os
118
custos de computação para as diferentes estratégias de disposição de checkpoints, desde
que o algoritmo tenha requerimentos de memória suficientemente baixos para evitar o
risco de swap da memória virtual para o disco.
O número de re-computações para cada nível i de checkpoints decresce a medida que i
cresce de 1 até L. Desta forma, pode ser estabelecido que a quantidade de computações
de linhas é dada por (3.11), adicionando-se 1 para corrigir o termo (para maiores
detalhes veja Wheeler e Hughey (2000)).
1=i
( ) ( )
+
−+++=
−+×+= ∑
= 1111
21
1 LLR
RLL
iiR
iRtL
iL (3.11)
3.3.6.1 Algoritmo Melhorado
Wheeler e Hughey (2000) apresentaram para r )()(1 RrnR LL <<− um algoritmo
denominado algoritmo melhorado, com ( )LL ,1− -níveis de checkpoints, que pode
melhorar muito a situação de perda de performance. Esse algoritmo melhorado executa
o algoritmo de ( 1)−L -níveis até que as primeiras ρ linhas da memória reservada para
checkpoints sejam preenchidas, tal que as linhas restantes de checkpoints sejam
suficientes para computar o restante da matriz de PD, usando o algoritmo de L-níveis.
De forma equivalente usa-se o algoritmo de ( )1−L -níveis para as primeiras ρ linhas de
memória tal que e o algoritmo de L-níveis para as últimas ( )ρrL−1 ( ) nρRrL >−+ ρ−R
linhas de memória.
A quantidade de computações de linhas efetuadas pelo algoritmo melhorado, de
-níveis de checkpoints, é dada por (3.12). O ρ ótimo pode ser encontrado em
tempo da ordem de O
( LL ,1− ))( nR loglog × , usando-se um algoritmo de busca com
complexidade exponencial (Wheeler e Hughey, 2000).
( ) ( ) ( )ρρ −+= −− RttRt LLLL 1,1 (3.12)
119
3.3.6.2 Algoritmo Ótimo
Entretanto os algoritmos melhorados, com ( )LL ,1− -níveis de checkpoints não são
ótimos. O algoritmo ótimo transita livremente entre níveis para fazer uso ótimo da
memória disponível para qualquer subproblema. Para estudar a disposição ótima dos
checkpoints Wheeler e Hughey (2000) utilizaram um algoritmo de programação
dinâmica de ordem O(n3).
Dadas as seqüências m e n, respectivamente, a maior e a menor seqüência, e a
quantidade de memória disponível R. Seja ( )nRC , a quantidade mínima de
computações de linhas. Então, para Rn ≤ tem-se que ( ) nnRC =, linhas e para os
demais casos, quando , a quantidade mínima de computação de linhas, , é
dado pela equação em (3.12), onde, o último termo i é o custo de computar até a linha i,
o termo C é o custo de retroceder da linha i até o início da seqüência e, o termo
é o custo de computar o restante da seqüência, após a linha i.
Rn > ( nRC , )
( ) 1, −iR
)in −,(RC −1
( ) ( ) ( ) iiRCinRCnRCni
+−+−−=≤<
1,,1min,0
(3.12)
Para a quantidade mínima de computação de linhas é dada pela equação em
(3.11),
( )Rrn L=
( ) ( )RtL=nRC , , ou seja, ( ) ( )LnOnRC =, linhas. Para pode-
se estabelecer, também usando a equação em (3.11), que
)()(1 RrnRr LL <<−
( ) ( )RtnR L 1, +C < , novamente
tem-se que ( ) ( ) ( )nO
( )n
LnO =
( ) OLnO =
nR =,
)( )n =1
C
( ) (LOn +=
linhas. Pode-se concluir, desta forma, que
linhas. RC ,
Considerando que, assintoticamente, o algoritmo melhorado com ( )LL ,1− -níveis de
checkpoints computa pelo menos ( )[ ]11 +− nL linhas e, que o algoritmo ótimo obtém
uma melhoria de no mínimo 1−L n linhas iniciais com relação ao algoritmo melhorado,
usando no lugar do algoritmo de nível L-1 pelo menos o algoritmo de nível 1. Desta
forma, assintoticamente, o algoritmo ótimo com ( )K,2−,1, − LLL -níveis de
checkpoints, computa pelo menos ( ) ( )[ ]12 −− L n1 −− LnL linhas.
120
Portanto, a razão da performance entre o algoritmo melhorado e o algoritmo ótimo é
pelo menos de ( )( ) ( ) 121
1−−−−
−L nLnL
nL , ou seja, é de ordem ( )1O . A razão da performance
entre o algoritmo original de L-níveis e o algoritmo ótimo, considerando que o
algoritmo de L-níveis computa Ln linhas, é pelo menos de ( ) ( ) 121 −−−− L nLnL
Ln , ou
seja, é de ordem
−1LLO .
3.3.7 Disposição Ótima de Checkpoints por Diagonais e o Algoritmo de Viterbi
Melhorado
A abordagem adotada por Wheeler e Hughey (2000) para melhorar a performance do
algoritmo com L-níveis de checkpoints por diagonais é similar àquela adotada para
checkpoints por linhas (colunas). Recorre-se à técnica de PD para determinar a
disposição ótima de checkpoints e tenta-se descobrir um algoritmo melhorado, mais
simples para uso geral.
Segundo Wheeler e Hughey (2000) para estudar a disposição ótima dos checkpoints por
diagonais é necessário um algoritmo de programação dinâmica de ordem O(n4). Apesar
de não apresentarem nada de concreto a respeito da modelagem do esquema de
disposição dos checkpoints, proporcionado (fornecido) por esse algoritmo, Wheeler e
Hughey (2000) relatam que essa modelagem é extremamente difícil e complicada. Mas
eles relatam que simplificaram o problema, considerando somente regiões triangulares
da matriz de PD, convenientes, para melhorar o algoritmo de Viterbi com checkpoints
por diagonais através dos resultados fornecidos e, produzindo um problema de PD de
O(n3). Entretanto novamente não apresentaram nem os resultados de experimentos ou
qualquer coisa de concreto a respeito desta modelagem.
Wheeler e Hughey (2000) relatam que o algoritmo melhorado com checkpoints por
diagonais, obtido da análise da disposição de checkpoints, tem desempenho pior que o
de seus competidores, o algoritmo de Powell (Powell et al., 1999). Entretanto eles não
121
descrevem esse algoritmo melhorado e sugerem que esse pode ser melhorado adotando
uma estratégia já adotada por Powell et al. (1999): mantêm-se duas variáveis adicionais
para guardar a linha da qual cada caminho parte na coluna zero e guardar em qual linha
cada caminho entra na última coluna. Durante a fase de retrocedimento pode-se omitir a
computação da porção final da matriz de PD, aquela compreendida entre a última linha
e a linha onde o melhor caminho deixou a última coluna da matriz, bem como a porção
inicial da matriz de PD, aquela compreendida entre a linha onde o melhor caminho
atingiu a primeira coluna e a primeira linha da matriz.
122
CAPÍTULO 4
ALGORITMOS PROPOSTOS
4.1 Introdução
No Capítulo 3 foram apresentados membros de uma família de métodos, que fazem uso
do método de PD e do paradigma (ou estratégia) de checkpoints, para comparação e
alinhamento de seqüências com economia de espaço – com necessidade reduzida de uso
de memória de trabalho – que podem ser usados com os algoritmos forward e backward
para determinar a P(O/λ), com o algoritmo de Baum-Welch (forward-backward) no
treinamento de perfis-HMMs e com o algoritmo de Viterbi para determinar o melhor
caminho. Essa família de métodos possibilita que o retrocedimento através da matriz de
PD possa ser efetuado, sem a necessidade de armazenar em memória toda a matriz de
PD, recorrendo-se ao uso de registro de informações do estado da computação a
intervalos regulares e apropriados, checkpoints, e de re-computação de seções da matriz
de PD. Eles requerem espaço da ordem de O( L nm ), permitindo uma economia de
espaço da ordem de ( )L n
,
O , na maioria das vezes, em troca de uma queda de
desempenho por um fator 10 ≤< kkL .
Nesse Capítulo são propostos algoritmos alternativos, denominados algoritmos com
checkpoints bidimensionais, que fazem uso da estratégia de checkpoints em conjunto
com o princípio D&C, através da disposição dos checkpoints em duas dimensões. A
disposição dos checkpoints em duas dimensões possibilita a utilização do princípio
D&C de maneira mais explicita e efetiva nesses algoritmos, sem a desvantagem da
recursividade presente no algoritmo de D&C de Hirschberg. Em contraposição ao
algoritmo de Hirschberg que é totalmente recursivo, os membros destes algoritmos
alternativos, propostos nesse trabalho, não recorrem a processos recursivos. Esses
algoritmos alternativos numa implementação serial mantêm o requerimento de memória
da ordem de O( L nm ) dos algoritmos que adotam o paradigma de checkpoints,
apresentados no Capítulo 3, com queda de desempenho por um fator proporcional a L.
123
Um dos membros desta família alternativa de algoritmos apropriado para determinar o
melhor caminho, denominado algoritmo de Viterbi com L-níveis de checkpoints
bidimensionais – por linhas e colunas simultaneamente – com retrocedimento restrito e
particionamento fixo de memória, apresenta uma performance superior àquela
apresentada pelo algoritmo de Viterbi com checkpoints por diagonais de L-níveis com
retrocedimento restrito e particionamento móvel de memória, numa análise de
desempenho a priori. Ele pode computar matrizes de PD maiores do que esse algoritmo
com checkpoints por diagonal e retrocedimento restrito, descrito no Capítulo 3, com
menor requerimento de memória, em troca de uma queda de desempenho, que em uma
análise a priori é menor e em uma análise posteriori é, também, suposta ser menor.
Esse algoritmo alternativo de Viterbi com checkpoints bidimensionais possibilita um
particionamento explicito da matriz de PD, o que significa condições de contorno bem
mais simples do que aquelas do algoritmo com checkpoints por diagonais. Desta forma
espera-se que a codificação deste algoritmo seja bem mais simples e que ele tenha um
melhor desempenho a posteriori.
Na Seção 4.2 são apresentados os membros da família de algoritmos de PD com L-
níveis de checkpoints que adotam uma disposição bidimensional destes checkpoints.
4.2 Algoritmos de L-Níveis de Checkpoints Bidimensionais
Nos algoritmos alternativos, propostos, denominados algoritmos com checkpoints
bidimensionais, foi adotada uma disposição dos checkpoints em duas dimensões, o que
possibilita a utilização do princípio D&C de maneira mais explicita e efetiva. Há várias
possibilidades de disposição dos checkpoints bidimensionais, nas duas dimensões da
matriz de PD. Eles podem ser dispostos nas duas dimensões na forma: linha × coluna;
diagonal × diagonal (ou anti-diagonal); linha × diagonal (ou anti-diagonal) e; coluna ×
diagonal (ou anti-diagonal). Em cada uma destas possibilidades pode-se imaginar várias
estratégias diferentes e promissoras. Nesse trabalho será explorado a disposição
bidimensional na forma de linha × coluna, pois essa evidencia, conforme pode ser
observado a seguir, ser a mais atraente e apresenta condições de contorno mais simples
para a programação do modelo.
124
Se for considerado apenas a disposição linha × coluna, pode-se dispor os checkpoints
conforme as figuras 4.1, 4.2, 4.3, 4.4, 4.5, 4.6 e 4.7. Pode-se imaginar algo semelhante
para as disposições diagonal × diagonal, linha × diagonal e coluna × diagonal. As 5
possibilidades apresentadas podem ser classificadas dentro de dois grupos. O primeiro
grupo refere-se às figuras 4.1, 4.2 e 4.3 e, o segundo grupo às figuras 4.4, 4.5, 4.6 e 4.7.
1 2 3 4 1 2 3 4 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 3 4 1 2 3 4 2 2 1 2 2 2 2 2 2 2 2 2 2 2 2 1 2 3 4 1 2 3 4 3 3 1 3 3 2 3 3 3 3 3 3 3 3 3 1 2 3 4 1 2 3 4 4 4 1 4 4 2 4 4 3 4 4 4 4 4 4 1 2 3 4 1 2 3 4 1 2 3 4
FIGURA 4.1 - Particionamento A: L-níveis de checkpoints completos e particionamento fixo de memória.
1 2 3 4 5 1 2 3 4 51 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 3 4 5 1 2 3 4 52 2 1 2 2 2 2 2 2 2 2 2 2 2 2 1 2 3 4 5 1 2 3 4 53 3 1 3 3 2 3 3 3 3 3 3 3 3 3 1 2 3 4 5 1 2 3 4 54 4 1 4 4 2 4 4 3 4 4 4 4 4 4 1 2 3 4 5 1 2 3 4 55 5 1 5 5 2 5 5 3 5 5 4 5 5 5
FIGURA 4.2 - Particionamento B: L-níveis de checkpoints completos e particionamento fixo de memória.
No primeiro grupo a matriz de PD é particionada em subseções por linhas × colunas
completas de checkpoints, criando partições mais homogêneas. Os particionamentos
bidimensionais das figuras 4.1 e 4.2 serão denominados particionamentos A e B – de L-
125
níveis de checkpoints bidimensionais completos de linhas por colunas e partição fixa de
memória. O particionamento bidimensional da figura 4.3 será denominado
particionamento C – de L-níveis de checkpoints bidimensionais completos de linhas por
colunas e partição móvel de memória.
1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 1 2 3 4 51 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 3 4 5 1 2 3 4 5 1 2 3 4 52 2 2 2 1 2 2 2 2 2 2 2 2 2 2 1 2 3 4 5 1 2 3 4 53 3 3 3 1 3 3 3 2 3 3 3 3 3 3 1 2 3 4 54 4 4 4 1 4 4 4 2 4 4 3 4 4 45 5 5 5 1 5 5 5 2 5 5 3 5 4 5
FIGURA 4.3 - Particionamento C: L-níveis de checkpoints completos e particionamento móvel de memória.
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 1 2 1 2 2 2 2 2 2 2 2 2 2 2 2 1 2 3 1 2 3 1 2 3 3 3 3 3 3 3 3 3 1 2 3 4 1 2 3 4 1 2 3 4 4 4 4 4 4 1 2 3 4 1 2 3 4 1 2 3 4 5
FIGURA 4.4 - Particionamento D: L-níveis de checkpoints parciais e particionamento fixo de memória.
No segundo grupo a matriz de PD é particionada em subseções por linhas × colunas
parciais de checkpoints, criando partições menos homogêneas, mas requerendo menos
espaço. Os particionamentos bidimensionais das figuras 4.4 e 4.6 serão denominados
particionamentos D e F – de L-níveis de checkpoints bidimensionais parciais de linhas
126
por colunas e partição fixa de memória – e os particionamentos bidimensionais das
figuras 4.5 e 4.7 serão denominados particionamentos E e G – de L-níveis de
checkpoints bidimensionais parciais de linhas por colunas e partição móvel de memória.
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 1 2 1 2 1 2 2 2 2 2 2 2 2 2 2 1 2 3 1 2 3 1 2 3 3 3 3 3 3 1 2 3 4 1 2 3 4 4 4 1 2 3 4 5
FIGURA 4.5 - Particionamento E: L-níveis de checkpoints parciais e particionamento móvel de memória.
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 1 2 1 2 1 2 3 3 3 3 3 3 3 1 2 3 1 2 3 1 2 3 4 4 4 4 1 2 3 4 1 2 3 4 1 2 3 4 5
FIGURA 4.6 - Particionamento F: L-níveis de checkpoints parciais e particionamento fixo de memória.
A princípio a escolha será pelos algoritmos do primeiro grupo, pois apesar de
requererem mais espaço que os do segundo grupo, possuem menos condições de
contorno a serem consideradas na programação do modelo e possibilitam uma queda de
desempenho menor quando comparados aos algoritmos de PD básicos. Por outro lado
os algoritmos do segundo grupo possibilitam a computação de matrizes de PD maiores,
127
usando praticamente a metade do espaço, do que aqueles algoritmos do primeiro grupo
podem computar usando o dobro de espaço.
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 1 2 1 2 1 2 3 3 3 3 1 2 3 1 2 3 4 4 1 2 3 4 5
FIGURA 4.7 - Particionamento G: L-níveis de checkpoints parciais e particionamento móvel de memória.
Os algoritmos do primeiro grupo requerem metade do espaço requerido pelo algoritmo
de Viterbi com L-níveis de checkpoints por diagonais com retrocedimento restrito e
particionamento móvel de memória e podem computar matrizes de dimensões duas
vezes maiores, para m , pois numa matriz n= nn × existem aproximadamente
diagonais. nnn 2=+
Outra questão a ser considerada é com relação ao particionamento da memória
disponível R entre memória para armazenar checkpoints e memória de trabalho. Os
algoritmos do primeiro grupo com L-níveis de checkpoints completos bidimensionais e
particionamento fixo de memória podem computar matrizes de PD até duas vezes
maiores do que os algoritmos com L-níveis de checkpoints completos bidimensionais e
particionamento móvel de memória. Adicionalmente, eles não apresentam o problema
de perda de performance devido a má disposição dos checkpoints apresentado pelos
algoritmos com L-níveis de checkpoints completos (bidimensionais ou unidimensionais)
e particionamento móvel de memória e, portanto não há necessidade do uso do
algoritmo melhorado de ( ) níveis,1 −− LL ou outro algoritmo melhorado.
128
A memória disponível MEM pode ser particionada de maneira análoga àquela do
algoritmo de PD com L-níveis de checkpoints unidimensionais e partição fixa de
memória. Ela pode ser dividida em dois segmentos: um segmento denominado Rcp para
armazenar os checkpoints e um segmento denominado Rtb usado para o cálculo de
seções da matriz de PD. A memória de trabalho, Rtb, sempre será de tamanho suficiente
para comportar no mínimo duas linhas da matriz de PD e a memória de checkpoints
deverá ter no mínimo L n linhas, sendo que é conveniente que sejam em número par.
A memória de trabalho precisa ter no mínimo duas linhas, , pois é necessário
manter a linha da matriz de PD que é anterior àquela que está sendo computada, pois as
recorrências do algoritmo de PD precisam dos valores contidos em células da linha
anterior. Por motivos que ficarão explícitos nos parágrafos seguintes, as linhas de
memória de trabalho R
2≥tbR
tb devem ser suficientes para, na fase de retrocedimento,
armazenar os cálculos ou os checkpoints de nível L-1 da ultima seção da matriz de PD,
aquela localizada no canto inferior direito da matriz e que contém a célula PDn,m,
delimitada pelos checkpoints bidimensionais. Portanto, às vezes, pode ser necessário
mais do que duas linhas de memória de trabalho.
Desta forma se 2=tbR linhas, então 22 −≥ Lcp nR linhas, e se linhas então 4=tbR
4−≥ Lcp nR linhas. Deste ponto em diante, supondo m = n, será assumido que 2=tbR
linhas e 22 −L n=cpR linhas, sendo que as linhas de memória Rcp serão divididas
igualmente em memória de checkpoints verticais (linhas) e horizontais (colunas), que
serão indistintamente chamadas de Rbd, ou seja, 12
22
−−
==L
cpbd
nRR 2
= L n .
Nessa família de algoritmos de PD com checkpoints de L-níveis bidimensionais, nos
membros com partição fixa de memória, os checkpoints em ambas as dimensões são
igualmente espaçados, ou seja, dado Lbd nR = linhas ou colunas e supondo , tem-
se que
nm =
Lbd n
nR
nc =+
=1
.
129
É importante lembrar, que linhas e que nesses algoritmos de PD com L-níveis
de checkpoints bidimensionais o procedimento de retrocedimento, também, só pode ser
iniciado após o cálculo do último valor (última célula) da matriz de PD, a célula PD
2=tbR
n,m.
4.2.1 Algoritmo F: 2-Níveis De Checkpoints Bidimensionais e Partição Fixa de
Memória
Para o algoritmo com 2-níveis de checkpoints bidimensionais e partição fixa de
memória o uso de R linhas de memória disponível pode ser otimizada fazendo-se
e 2=tbR 12
222
2−=
−=
−= nnRRbd . Entretanto . 962 ≥⇒≥⇒≥ nRRbd
Supondo , tem-se, então, que a distância entre dois checkpoints sucessivos é nm =
nn
=1
cc ,3,2
. Desta forma na primeira fase de computação da matriz de PD
para frente (forward) usam-se as duas linhas da memória de trabalho, , para
computar todas as linhas da matriz e salvam-se somente os valores das células
correspondentes às linhas ou colunas de checkpoints, a saber: as células pertencentes às
linhas 1 e as células pertencentes às colunas 1 .
2=tbR
cbdcRc bd,, K Rccc ,,3,2, K
nR
ncbd
=+
=
O processo de retrocedimento inicia-se na última seção da matriz de PD que contém a
célula PDn,n, aquela compreendida entre os checkpoints - localizados na linha
e na coluna - e a última linha e coluna da matriz. Na fase de retrocedimento
são usadas apenas as linhas da memória de trabalho,
cRbd
2
cRbd cRbd
=tbR , para re-computar todas as
linhas de cada seção, armazenar todos os resultados necessários e então executar o
procedimento de retrocedimento nessa seção.
Se o retrocedimento for completo, então, após o procedimento sobre a última seção, re-
computa-se os resultados da seção à esquerda a partir dos checkpoints (horizontal)
e (vertical) e executa-se o procedimento de retrocedimento nessa seção.
Repete-se esse processo até que a primeira coluna seja alcançada. Em seguida, computa-
cRbd
1−cRbd
130
se as seções compreendidas entre os checkpoints horizontais e e executa-
se o procedimento de retrocedimento nessas seções, iniciando-se naquela seção
compreendida entre o checkpoint vertical e a última coluna terminando na seção
compreendida entre o checkpoint vertical c e a primeira coluna. Esse processo é
repetido até que a primeira seção da matriz de PD seja alcançada e o procedimento de
retrocedimento seja executado nessa primeira seção, completando-se, portanto, o
retrocedimento sobre a matriz de PD.
cRbd 1−cRbd
cRbd
R4
2
Se o retrocedimento for parcial, então, após o procedimento sobre a última seção, re-
computa-se os resultados da seção adjacente à linha ou coluna de checkpoint que o
alinhamento ótimo alcançou no procedimento de retrocedimento sobre a seção anterior
e, então, executa-se o procedimento de retrocedimento sobre essa seção. Procede-se de
maneira análoga para todas as seções adjacentes até que a primeira seção seja atingida,
aquela localizada no canto superior esquerdo, e seja executado o procedimento de
retrocedimento sobre ela, completando-se, assim, o retrocedimento parcial sobre a
matriz de PD.
Considerando a suposição já assumida de que m n= , então o maior número de linhas
que o algoritmo de 2-níveis com partição fixa pode calcular com R linhas é
( ) ( )42
122
2 RRRRrn bdL =
=+== linhas.
A análise da performance deste algoritmo é simples de ser feita. Seja n= , então
tem-se nR 2= checkpoints. De onde, pode-se concluir que o algoritmo de 2-níveis
com partição fixa tem complexidade de espaço da ordem de ( )nnO .
Com relação à complexidade de tempo do algoritmo de 2-níveis com partição fixa tem-
se que são calculadas n2 células da matriz de PD numa execução para frente (forward)
do algoritmo de PD. Se o processo de retrocedimento é completo, então são computadas
( ) nnnnnnnn 232 2222 −=+−+ células, contra do algoritmo básico. Se o 22n
131
retrocedimento é parcial, então são computadas no máximo
( ) nnnnnnnn 2222 222 ++=+
+ células da matriz de PD, contra do
algoritmo básico.
nn 22 +
( )2n n
n
Desta forma, considerando que se gaste um tempo unitário para calcular cada célula da
matriz de PD, tem-se que o algoritmo de 2-níveis com partição fixa tem complexidade
de tempo da ordem de O com queda de desempenho de nn 22 − para o
retrocedimento completo e com uma penalidade de desempenho de n2 para o
retrocedimento parcial.
4.2.2 Algoritmo G: 3-Níveis de Checkpoints Bidimensionais e Partição Fixa de
Memória
Uma versão 3-níveis do algoritmo F pode reduzir o uso de memória ainda mais, ou seja,
com a mesma quantidade de memória usada pelo algoritmo F pode-se calcular matrizes
de PD ainda maiores com uma versão de 3-níveis com partição fixa de memória. Esse é
um algoritmo de 3-níveis de checkpoints bidimensionais que realiza: computação e
armazenagem de checkpoints bidimensionais de nível-3; computação e armazenagem
provisória de checkpoints bidimensionais de nível-2 (algoritmo F); e a computação e
armazenagem provisória de checkpoints de nível-1 (algoritmo de PD básico).
Para o algoritmo com 3-níveis de checkpoints bidimensionais e partição fixa de
memória o uso de R linhas de memória disponível pode ser otimizada fazendo-se
e 2=tbR 12
222
2 33
−=−
=−
= nnRRbd . Entretanto . 2762 ≥⇒≥⇒≥ nRRbd
Supondo , tem-se, então, que a distância entre dois checkpoints sucessivos é nm =
31 nn
= . Quando é necessário efetuar um procedimento de
retrocedimento total sobre a matriz de PD com um algoritmo de 3-níveis de checkpoints
e partição fixa de memória, usam-se as duas linhas da memória de trabalho, , 2=tbR
3 2nR
ncbd
=+
=
132
para computar e armazenar checkpoints bidimensionais de nível-3 na matriz de PD
completa. Com as mesmas linhas da memória de trabalho, 2=tbR , que são agora
particionadas ao meio em memória de trabalho e memória de checkpoints de nível 2, re-
computa-se e armazena-se os checkpoints bidimensionais de nível-2 (algoritmo F) em
cada seção da matriz de PD delimitada pelos checkpoints bidimensionais de nível-3,
começando sempre na última seção, aquela localizada no canto inferior direito. Com as
mesmas linhas da memória de trabalho de nível 2, que são agora particionadas e usadas
como memória de checkpoints de nível 1, re-computa-se e armazena-se os checkpoints
de nível-1 (algoritmo de PD básico) em cada seção da matriz de PD delimitada pelos
checkpoints bidimensionais de nível-2, começando sempre na última seção.
Então, o processo de retrocedimento completo sobre a matriz de PD é efetuado da
seguinte forma. Uma vez re-computados e armazenados os checkpoints de nível-1 numa
última seção, executa-se, então, o retrocedimento sobre essa seção de nível 1 e passa-se
para a próxima seção adjacente de nível 1 (localizada à esquerda), e então procede-se de
forma regular e sucessiva (caminhando sempre da direita para a esquerda) passando-se
para a próxima seção até que essas seções de nível 1, contidas numa dada seção de nível
2, sejam esgotadas e o retrocedimento sobre essa seção de nível 2 esteja completo. De
maneira análoga procede-se, de forma regular e sucessiva (caminhando sempre da
direita para a esquerda), à re-computação e retrocedimento sobre cada seção de nível 2
delimitada pelos checkpoints de nível 3 até que todas elas sejam esgotadas e o
retrocedimento sobre a matriz de PD esteja, desta forma, completo.
O processo de retrocedimento parcial é efetuado da seguinte forma. Uma vez
computados e armazenados os checkpoints de nível-3, o processo de retrocedimento
parcial inicia-se na última seção de nível 2 da matriz de PD, que contém a célula PDn,n.
Re-computa-se, então, os resultados da seção de nível 2 adjacente à linha ou coluna de
checkpoint que o alinhamento ótimo alcançou no procedimento de retrocedimento sobre
a seção de nível 2 anterior, e então executa-se o procedimento de retrocedimento sobre
essa seção. Procede-se de maneira análoga para todas as seções de nível 2 adjacentes até
que a primeira seção de nível 2 seja atingida, aquela localizada no canto superior
esquerdo, e executado o procedimento de retrocedimento sobre ela. Completando-se,
133
assim, o retrocedimento parcial sobre a matriz de PD. Para executar o processo de
retrocedimento nessas seções de nível 2 chama-se o algoritmo F – de 2-níveis de
checkpoints bidimensionais e partição fixa de memória – usando as mesmas linhas da
memória de trabalho, , que são agora particionadas ao meio em memória de
trabalho e memória de checkpoints de nível 2.
2=tbR
Considerando a suposição já assumida de que m n= , então o maior número de linhas
que o algoritmo de 3-níveis com partição fixa pode calcular com R linhas é
( ) ( )82
133
33
RRRRrn bdbd =
=+== linhas.
A análise da performance deste algoritmo é simples de ser feita. Seja nR=
8
3
, então
tem-se que 32 nR = . De onde pode-se concluir que o algoritmo de 3-níveis com
partição fixa tem complexidade de espaço da ordem de ( )3 nnO .
Com relação à complexidade de tempo do algoritmo de 3-níveis de checkpoints
bidimensionais com partição fixa tem-se que são calculadas n2 células da matriz de PD
numa execução para frente (forward) do algoritmo de PD. Se o processo de
retrocedimento é completo, então são computadas
( ) 322322 2422 nnnnnnnn −=+−+ células, contra do algoritmo básico. Se o
retrocedimento é parcial, então são computadas, no máximo,
22n
( ) nnnnnnnn 24222 3 222
3 232 ++=+
+ células da matriz de PD, contra do
algoritmo básico.
nn 22 +
Desta forma, considerando que se gaste um tempo unitário para calcular cada célula da
matriz de PD, tem-se que o algoritmo de 3-níveis com partição fixa tem complexidade
de tempo da ordem de ( )2nO com queda de desempenho de 32 22 nnn − para o
retrocedimento completo e com uma penalidade de 3 24 nn para o retrocedimento
parcial.
134
4.2.3 Algoritmo H: L-Níveis de Checkpoints Bidimensionais e Partição Fixa de
Memória
Nessa seção considera-se um algoritmo com nível de recursão arbitrário e
particionamento fixo de memória; o algoritmo de L-níveis de checkpoints
bidimensionais, por linhas e colunas, com particionamento fixo de memória. Essa
versão L-níveis do algoritmo G pode reduzir o uso de memória ainda mais, à medida
que L cresce.
Para o algoritmo com L-níveis de checkpoints bidimensionais e partição fixa de
memória o uso de R linhas de memória disponível pode ser otimizada fazendo-se
e 2=tbR 12
222
2−=
−=
−= L
L
bd nnRR . Entretanto . Lbd nRR 362 ≥⇒≥⇒≥
Supondo , tem-se, então, que a distância entre dois checkpoints sucessivos é nm =
L nn
1= . L L
bd
nR
nc 1−=+
=
O número máximo de linhas, , que podem ser computadas com o algoritmo de L-
níveis bidimensionais com partição fixa de memória para uma determinada quantidade
de memória disponível de R checkpoints é dado por:
( )RrL
( ) ( )L
LbdbdL
RRRr
=+=
21 . (4.1)
A análise da performance deste algoritmo é simples de ser feita. Seja nRL
L
=2
, então
tem-se que L nR 2= . De onde pode-se concluir que o algoritmo de L-níveis com
partição fixa tem complexidade de espaço da ordem de ( )L nnO .
Com relação à complexidade de tempo do algoritmo de L-níveis de checkpoints
bidimensionais com partição fixa tem-se que são calculadas n2 células da matriz de PD
numa execução para frente (forward) do algoritmo de PD. Se o processo de
135
retrocedimento é completo, então ele precisa ser executado em todas as células da
matriz de PD, logo, são computadas ( )( ) ( ) LL nLnnLnnLnnLn −+=+−−+ 2222 11
( )
células, contra do algoritmo básico. Se o retrocedimento é parcial, então são
computadas, aproximadamente,
22n
nnnnLnnLL L L
L LL 22221 222
21
++≤+
− −
−
LLn 23 ≥≥
n2 +
células da matriz de PD. Mas como então são computadas, no máximo,
nnnn L L 222 2222 +++ − n2 ≤ células da matriz de PD, contra do algoritmo
básico.
nn 22 +
Desta forma, considerando que se gaste um tempo unitário para calcular cada célula da
matriz de PD, tem-se que o algoritmo de L-níveis com partição fixa tem complexidade
de tempo da ordem de ( )2nO com queda de desempenho de ( ) L nLnnL −− 21
computações adicionais de células de PD para o retrocedimento completo e, com uma
penalidade de cálculo de células de PD para o retrocedimento parcial. 2n
Numa análise de pior caso, quando L = n, temos que a complexidade de tempo é da
ordem de ( )2nO para o algoritmo com retrocedimento parcial, com um coeficiente igual
a 2, e de ( )3nO para o com retrocedimento completo. A complexidade de espaço é de
ordem O(n).
4.2.4 Algoritmo J: Viterbi com L-Níveis de Checkpoints Bidimensionais e
Partição Fixa de Memória
A performance do algoritmo de Viterbi para calcular o melhor caminho através da
matriz de PD pode ser melhorada aplicando a esse o princípio D&C. Grice et. al (1997)
aplicaram o princípio D&C no algoritmo de Viterbi com L-níveis de checkpoints por
diagonais. Esse algoritmo é referido como algoritmo de PD com L-níveis de checkpoints
por diagonais e retrocedimento restrito e trata-se do algoritmo E da seção 3.3.5.
Tarnas e Hughey (1998) implementaram o loop interno do pacote SAM com o
algoritmo de PD com L-níveis de checkpoints por diagonais, mas, após avaliações e
136
ancorados na experiência com esses algoritmos, ficaram desapontados com as versões
destes algoritmos por diagonais e planejaram re-implementar as versões de checkpoints
por linhas. Eles avaliaram que a complexidade adicional, devido às condições de
contorno para o processamento de diagonais e armazenamento de checkpoints por
diagonais, não proporcionou um ganho de performance equivalente, mesmo quando
usando o algoritmo de Viterbi com L-níveis de checkpoints por diagonais e
retrocedimento restrito.
O princípio D&C e o paradigma de checkpoints também podem ser aplicados,
conjuntamente, aos algoritmos de PD considerando-se checkpoints em duas dimensões,
usando-se checkpoints por linhas e colunas. Essa seção trata do algoritmo de Viterbi
com L-níveis de checkpoints, completos, bidimensionais por linhas e colunas e
particionamento fixo de memória.
O uso de checkpoints por linhas e colunas simultaneamente não acarreta ineficiências
com relação ao requerimento de espaço. Necessita-se de um número de checkpoints L nLR ≤ para armazenar os checkpoints nas duas dimensões, na das linhas e na das
colunas. Além do que o algoritmo de Viterbi com L-níveis de checkpoints
bidimensionais, para uma dada quantidade de memória disponível Rbd, possibilita a
computação do melhor caminho numa matriz de PD de dimensões
( ) ( )L
LbdbdL
RRRrn
=+==
21 , considerando-se nm = , maior do que aquela que o
algoritmo com L-níveis de checkpoints por diagonais com retrocedimento restrito e
particionamento móvel de memória pode calcular com uma dada quantidade de
memória disponível R (>Rbd).
O tempo para se calcular o melhor caminho através da matriz de PD é a soma do tempo
no primeiro passo para frente durante a computação da matriz de PD, mais o tempo
gasto no retrocedimento. Usando-se o paradigma de checkpoints, com checkpoints
bidimensionais por linhas e colunas e particionamento fixo de memória, gasta-se um
tempo n2 na primeira fase, na computação da matriz de PD e armazenagem ou
salvamento dos checkpoints. Mas no passo de retrocedimento pode-se economizar
137
tempo de re-computação aplicando o princípio D&C, pois não é necessário re-computar
todas as subseções formadas pela disposição dos checkpoints na matriz de PD, mas
apenas uma fração destas subseções.
Supondo-se em primeiro lugar que exista uma relação um a um entre os pontos de uma
grade ou treliça e as células da matriz de PD, em segundo lugar que o espaçamento ou
distância entre dois checkpoints sucessivos seja c, em ambas dimensões ( , e em
terceiro lugar que se o melhor caminho atinje o checkpoint k em uma das dimensões
(vertical ou horizontal), na i-ésima linha ou j-ésima coluna, então o melhor caminho
precisa alcançar ou o checkpoint (na
)nm =
k ( )i -ésima linha ou na ( )j -ésima coluna) ou o
checkpoint (na ( -ésima linha ou na 1−k )ci − ( )cj − -ésima coluna). Portanto, é
necessário re-computar somente uma região retangular contendo células da
matriz de PD.
(c )21−
Devido ao fato de que se pretende comparar esse algoritmo J com o algoritmo E da
seção 3.3.5, então será considerado uma quantidade de memória, supostamente, igual à
requerida por aquele algoritmo, mas esse pode trabalhar com a metade desta memória.
Desta forma, seja LL nLnLR 42!2 ≤= .
Tomando-se L nLR 4≤ , então existem no máximo L nL4 regiões (subseções)
contendo células da matriz de PD, que necessitam ser re-computadas. É
necessário observar que, no algoritmo com partição fixa de memória, a distância entre
checkpoints em ambas as dimensões, supondo
( )21−c
nm = , é dada para todas as regiões
(subseções) delimitadas por checkpoints pela seguinte equação:
LL
Ln
LnLnc
1
21
2
−
== . (4.2)
Desta forma, considerando que para um nível arbitrário L existem L nL2 checkpoints
em cada uma das dimensões, com um espaçamento entre eles de LL
nL
1
21 −
, então na fase
138
de retrocedimento são re-computadas L nL4 seções de dimensões
×
−−L
LL
L
nnL
11
241
( )nL
da
matriz de PD usando o algoritmo de nível L-1. Logo, o tempo total, T , gasto pelo
algoritmo J, de L-níveis bidimensionais (por linhas e colunas) e partição fixa de
memória, para calcular o melhor caminho, pode ser estimado em termos de
complexidade assintótica conforme mostra a desigualdade (4.3), dada a seguir.
( )
( )2nO
On =
( )
+=
+≤
−
−
−
−L
L
L
LL
L
LL
L nTLnnnT
LnLnnT
1
12
1
122
414 (4.3)
A partir da equação (4.3) pode-se mostrar por indução que o tempo total, T , gasto
pelo algoritmo J – de L-níveis de checkpoints bidimensionais (por linhas e colunas) e
partição fixa de memória – para calcular o melhor caminho tem complexidade
assintótica da
( )nL
( )2nO , quando . Veja a prova nas equações em (4.4), a seguir. nL blog≤
( ) ( ) ( )( ) ( ) ( ) ( )
( ) ( ) ( ) ( )
( ) ( ) ( ) ( )
( ) ( ) ( ) ( )
( ) ( ) ( )
( )
( ) ( ) ( )2log1log
2
1log2
1log
log2
log
log2
1log
25
24
52
5
24
23
42
4
222
22
32
22
221
22
21
21
log
blog
blog
n quemostrar se-deve , que se-Supondo
120602055
241244
63233
222
log1log
57
58
59
54
46
47
43
35
32
323
1
32
212
1
21
nOnTnTn
n
bnT
nnnT
nnnT
nTnOnT
nOnTnnnnnnTnnnT
nOnTnnnnnTnnnT
nTnnnnnnnnTnnnT
nOnTnnnnnnTnnnT
nOnTnnT
nnb
nb
nb
n
n
nn
bb
b
nb
nb
b
b
b
bb
=∴+≤
≤
+=
+≤
==
=∴++++=+≤
=∴+++=+≤
∴++=
++=+≤
=∴+=×+=+≤
=∴=
−
−−
−
−
M
33
(4.4)
Pode ser observado das equações em (4.4) que, numa análise a priori, assintoticamente
não existe penalidade de tempo, para níveis de checkpoints nL blog≤ . Entretanto, essa
139
análise descarta os coeficientes multiplicativos e, como os algoritmos são da mesma
ordem de complexidade, é necessário ser melhor elucidado a dimensão desta
penalidade, levando-se em consideração esses coeficientes. Desta forma pode-se
estabelecer um fator de queda de desempenho com base nesses coeficientes.
No estudo deste fator ou coeficiente de queda de desempenho pode ser deduzido das 5
primeiras equações em (4.4) que a desigualdade em (4.3) tem equação de recorrência,
dada a seguir em (4.5), composta de L fatores no lado direito, que incluem
necessariamente o primeiro e o último fator.
( ) ( ) ( )( ) ( ) ( )( ) ( )2213211
22212
2
LLL
−−+
−−++
−++≤
+−−
LLLn
LLLn
LLn
LnnnT
LL
LL
LL
L (4.5)
Fazendo em (4.5) e considerando logaritmos de base 2, bastante usados em
biologia computacional, e logaritmos de base 10 e 100, obtém-se as desigualdades que
são mostradas em (4.6).
nL blog=
Se for considerado em (4.6) somente a equação para logaritmos de base 2, pode ser
mostrado que a soma dos termos de segundo grau, , tende a 1 quando n cresce – veja
as equações dadas em (4.7). De onde, pode-se concluir que a queda de desempenho para
o nível , quando calculando o melhor caminho com o algoritmo de PD com
checkpoints bidimensionais (por linhas e colunas) e partição fixa de memória, não tem
penalidade de tempo, para n grande.
2n
nL 2log=
Se forem consideradas em (4.6) as equações para logaritmos de base 10 e 100, pode ser
mostrado, de maneira análoga às equações em (4.7), que a soma dos termos de segundo
grau, , tende a 1 quando n cresce. De onde, pode-se concluir que a queda de
desempenho para os níveis
2n
nLnLnL 100102 log e log ,log ===
nL blog
, quando calculando o
melhor caminho com o algoritmo de PD com checkpoints bidimensionais (por linhas e
colunas) e partição fixa de memória, não tem penalidade de tempo, para n grande. Pode-
se mostrar usando indução que, para um nível = , o coeficiente de tende a 1
para n grande, ou seja, não se tem penalidade de tempo quando se usa o algoritmo de
2n
140
PD com checkpoints bidimensionais (por linhas e colunas) e partição fixa de memória
com um nível de checkpoints . nL blog≤
( )
( )
( )
( ) )(
( ) log3
2
log100
100, base de log3
2
log10
10, base de 24
loglog
2, base de
log
100
1002
10
102
2
2
22
2
log2log2
n
nn
n
nn
nn
nnn
L
bb
nb
nb
+
+−
+
+
+−
+
−
−
−
L
L
L
( )
(4.6)
( ) ( ) ( )
( )
( ) ( ) ( )( )
( ) ( ) ( ) ( )
( ) ( )
( ) ( ) ( )
( ) ( )
( ) ( )
( ) ( )
( ) ( )!1loglog100
41loglog100log1loglog100
1loglog100
que se- temlogaritmos se-doConsideran!1loglog10
41loglog10log1loglog10
1loglog10
que se- temlogaritmos se-doConsideran!log31loglog1loglog2
2log1loglog212log2
:que se- temlogaritmos se-doConsideran!log
31loglog1loglog
1001001002
2100100
3
2
1001003
2100
2
100
22
log
10102
21010
3
2
1010103
21010
22
log
222
2
223
2222
3
2
22
22
log
2log
100
10
2
log2log
log1log2
nn
nnn
nnn
nnn
nnn
nnnnT
nnn
nnn
nnn
nnnnnT
nn
nnn
nnn
nnnn
nnnnnT
nn
nnn
nnnnT
L
L
n
L
L
n
L
n
b
bbbn
nb
nbnb
nb
b
−+
+−
+−
+
+−
+≤
−+
+−
+−
+
+−
++≤
+−−
++
+−−
+−
++≤
+
+−
++++≤
−
−
−
−
−
+−
LL
L
L
LL
LL
Do exposto nos parágrafos anteriores pode-se concluir que não há queda de desempenho
para o algoritmo com L-níveis de checkpoints bidimensionais (por linhas e colunas),
quando o número de níveis for da ordem ( )nO blog
L blog
e desde que haja memória
disponível suficiente para ser utilizada com esse nível e tamanho da matriz de PD, o que
supera o desempenho assintótico do algoritmo de Viterbi com L-níveis de checkpoints
por diagonais e retrocedimento restrito, para n≤ . É bom salientar que essa
141
situação é ainda mais confortável quando incorpora-se o retrocedimento restrito ao
algoritmo J.
( ) ( ) ( )( )
( ) ( ) ( ) ( ) ( )
( )
( )
( ) ( ) ( )
( ) ( ) ).( grande é quando ,!log
.0log
1 :que se- tem)( grande Para
!loglog11
!log1
log
!log21
21
21
21
21
log
!loglog2log2log2log2log2
!log31loglog241loglog2
2log1loglog21loglog2log2
2
2log
2
22
2log
22
22
22332
2
22
222
2
23
2
23
2
22
2
2
22
2222
2
223
2222
3
2
222
2
2
22
log
2
2
2
∞→+≅∴
→∞→
+
+≤∴+×+≤
≤+
++++++=
+++++++≤
≤+−
+−
++
+−−
+−
++≤
−−
−−
−−
nnn
nnnT
nnn
nn
nnnT
nn
nnn
nn
nnn
nn
nn
nn
nn
nn
nnn
nn
nnn
nnn
nnnn
nnn
nnnnT
n
n
LL
LL
LL
n
L
L
LLL
(4.7)
O algoritmo J de Viterbi com L-níveis de checkpoints bidimensionais (por linhas e
colunas) e particionamento fixo de memória, para calcular o melhor caminho, apresenta
comportamento de requerimento de espaço semelhante aos demais membros desta
família de algoritmos. Seja o espaço necessário para armazenar checkpoints de
nível L suficientes para que se faça o retrocedimento sobre a da matriz de PD contendo
n linhas. O requerimento de espaço do algoritmo de nível 1é
( )nSL
( ) 21 nnS = . O algoritmo de
nível 2 necessita de espaço para ( ) ( ) ( )nnOn =SnnnS ∴≤ 22 4 locações de
memória. O algoritmo de nível 3 necessita de espaço para
( ) ( ) ( )3 nn33 6 OnSnnnS =∴≤ locações de memória. De maneira geral o
requerimento de memória do algoritmo de L-níveis pode ser dado pela equação a seguir.
( ) LL nLnnS 2≤ (4.8)
142
Da desigualdade em (4.8) pode-se concluir que para o requerimento de espaço do
algoritmo J de Viterbi com L-níveis de checkpoints bidimensionais (por linhas e
colunas) e particionamento fixo de memória é da ordem de
1≥L
( ) ( )LL nnOnS = .
Desta forma o algoritmo J de Viterbi com L-níveis de checkpoints bidimensionais (por
linhas e colunas) e particionamento fixo de memória apresenta requerimento de espaço,
quando se toma nL blog= níveis, da ordem de ( ) ( )nnOnS bL log= .
Esse algoritmo de Viterbi com nL blog≤ níveis de checkpoints bidimensionais (por
linhas e colunas) e particionamento fixo de memória, que faz uso explícito do princípio
D&C, para calcular o melhor caminho através da matriz de PD aproxima a performance
assintótica do algoritmo de Hirschberg (totalmente baseado na estratégia D&C) em
termos de requerimento de espaço de ( )nO e iguala a performance com relação ao
tempo de execução de ( )2nO , mas superando o desempenho por um fator de 2. Ele,
também, apresenta desempenho semelhante no requerimento de espaço e superior no
requerimento de tempo de execução, por um fator de 2, ao do algoritmo de Viterbi com
L-níveis de checkpoints por diagonais e retrocedimento restrito, para , que
integra o estado da arte com respeito a essa família de algoritmos de PD com L-níveis
de checkpoints.
nL blog≤
143
144
CAPÍTULO 5
EXPERIMENTAÇÃO E ANÁLISE DOS RESULTADOS
5.1 Introdução
Nesse capítulo são apresentados alguns detalhes da implementação dos algoritmos e da
realização dos experimentos previstos na Seção 4.3 para análise de performance a
posteriori dos seguintes algoritmos de PD: algoritmo de Viterbi básico; algoritmo de
Viterbi com 2-níveis de checkpoints por linhas e particionamento móvel de memória;
algoritmo de Viterbi com 2-níveis de checkpoints por diagonais, com retrocedimento
restrito e particionamento móvel de memória; e algoritmo de Viterbi com 2-níveis de
checkpoints bidimensionais por linhas e colunas, com retrocedimento restrito e
particionamento fixo de memória. São apresentados, também, os resultados dos
experimentos e a análise destes resultados.
A Seção 5.2 trata da metodologia de experimentação. A Seção 5.3 trata da
implementação dos algoritmos e da realização dos experimentos. Os resultados e a
análise dos experimentos são apresentados na Seção 5.4.
5.2 Metodologia de Experimentação
Seqüências biológicas referem-se ao encadeamento de resíduos que formam a estrutura
primária de determinadas macromoléculas que fazem parte do metabolismo dos seres
vivos. Elas são polímeros formados pelo encadeamento de 4 nucleotídeos ou de 20
aminoácidos. Dentre e as de maior interesse estão as proteínas e os ácidos nucléicos
(ácidos ribonucléicos - RNA - e ácidos desoxirribonucléicos - DNA). As moléculas
(seqüências) de de DNA contém subseqüencias denominadas genes que codificam a
informação necessária para a fabricação das proteínas do organismos. Várias moléculas
de DNA são agrupadas para formarem os cromossomas, e o conjunto de todos os
cromossomas formam o genoma de um organismo.
Das estatísticas publicadas pelo Swiss-Prot group (2004) sabe-se que as moléculas de
proteínas, anotadas e contidas no banco de dados Swiss-Prot, são compostas em média
145
por seqüências de 368 aminoácidos, sendo a menor seqüência composta por 2
aminoácidos e a maior por 8.797 aminoácidos. Veja na Figura 5.1 a seguir o gráfico da
distribuição do tamanho das seqüências de proteínas. Esses dados motivam a realização
FIGURA 5.1 - Distribuição do tamanho das seqüências de proteínas.
de experimentos para o alinhamento de seqüências biológicas relativamente pequenas.
FONTE: Adaptada de figura do Swiss-Prot group (2004).
Segundo 01) o
tamanho médio de um gene consiste do encadeamento de 3.000 bases, formando uma
dados do Human Genome Mangement Information System (1992 e 20
seqüência primária. O maior gene humano conhecido, o dystrophin, é composto por 2,4
milhões de bases. As moléculas de DNA no genoma humano são arranjadas em 24
cromossomas distintos com tamanhos variando de 50 a 250 milhões de pares de bases.
Os genomas dos diferentes organismos possuem uma grande variação em tamanho, o
menor genoma conhecido contêm cerca de 600 mil pares de bases (bactéria), enquanto
os genomas do rato e do homem contêm cerca de 3 bilhões de pares de bases. Esses
dados motivam a realização de experimentos para o alinhamento de seqüências
biológicas relativamente grandes, pois já se fala em comparação de genomas inteiros.
146
Foi adotada nesse trabalho uma posição semelhante àquela adotada por Tarnas e
Hughey (1998), pag. 404: “... Para SAM rodando numa workstation, foi decidido que o
algoritmo de nível , que reduz o uso de espaço pela raiz quadrada do
comprimento da seqüência, deveria ser suficiente ...”. Pois experimentos, para a análise
de performance a posteriori, com algoritmos de PD com 2-níveis de checkpoints,
permitirão comparações com a análise de performance a posteriori já realizadas e
relatadas por Grice et al. (1995 e 1997), Tarnas e Hughey (1998) e Wheeler e Hughey
(2000).
2=L
Tendo como base os relatos em Grice et al. (1995 e 1997), Tarnas e Hughey (1998) e
Wheeler e Hughey (2000) e a pesquisa efetuada nesse trabalho, pode-se concluir que,
quando o interesse reside justamente na utilização dos algoritmos de PD de L-níveis de
checkpoints em computações que exijam retrocedimento completo, a escolha deve
recair naqueles com checkpoints unidimensionais por linhas ou por colunas. Isto é o
melhor que se pode vislumbrar até o momento, a não ser que alguma percepção nova
lance luz sobre o assunto, pois as demais versões são mais dispendiosas em termos de
espaço e de tempo – exigem o dobro de memória e adicionam mais complexidade no
tratamento e armazenagem dos checkpoints e nas re-computações das seções da matriz
de PD. Entretanto, os resultados da análise a priori dos algoritmos de L-níveis de
checkpoints bidimensionais indicam que esses assintoticamente requerem menos espaço
do que os algirmos de checkpoints por linhas e apresentam menor queda de
desempenho.
Desta forma os experimentos para análise de performance a posteriori se concentraram
somente nos algoritmos de Viterbi com L-níveis de checkpoints, particularmene nos de
2-níveis de checkpoints.
Os algorimos de PD que adotam o paradigma de checkpoints como estratégia, ou
método de economia de espaço, permitem uma amplitude de escolhas dentro do
intervalo de espaço disponível, que possibilitam fazer o balanceamento (tradeoff) entre
requerimentos de espaço e de tempo para computar uma determinada matriz de PD.
Para HMMs com um determinado número de nós e tamanhos de seqüências a serem
147
analisadas, a penalidade de performance pode ser escolhida, conforme o número níveis
de checkpoints adotados, levando-se em consideração toda a hierarquia de memória e,
principalmente, a quantidade de memória de trabalho disponível – primariamente a
memória RAM e secundariamente a memória cache.
Os experimentos objetivaram comparar o desempenho dos seguintes algoritmos, no
alinhamento de seqüências biológicas pequenas (seqüências de proteínas com tamanho
médio de 368 símbolos e um alfabeto de 20 símbolos) e grandes (seqüências de DNA
contendo milhôes de símbolos e um alfabeto de 4 símbolos): o de Viterbi básico; o de
Viterbi com 2-níveis de checkpoints por linhas e particionamento móvel de memória; o
de Viterbi com 2-níveis de checkpoints bidimensionais por linhas e colunas com
retrocedimento restrito e particionamento fixo de memória; e o de Viterbi com 2-níveis
de checkpoints por diagonais com retrocedimento restrito e particionamento móvel de
memória.
No alinhamentos de seqüências grandes pretendeu-se verificar como os algoritmos
desempenham até o início de paginação. Também pretendeu-se verificar para que
tamanhos de seqüências esses algoritmos causam paginação excessiva num PC típico,
inviabilizando a obtenção de resultados para seqüência acima deste tamanho.
Finalmente, verificar se o algoritmo de Viterbi com checkpoints bidimensionais por
linhas e colunas com retrocedimento restrito e particionamento fixo de memória utiliza
eficientemente o sistema de memória, particularmente no uso da memória cache.
Entretanto o interesse maior residiu justamente na comparação entre o algoritmo de
Viterbi com 2-níveis de checkpoints bidimensionais por linhas e colunas com
retrocedimento restrito e particionamento fixo de memória e o algoritmo de Viterbi com
2-níveis de checkpoints por diagonais com retrocedimento restrito e particionamento
móvel de memória.
Para uma melhor discriminação na comparação entre esses dois algoritmos de interesse
primário nos experimentos foram utilizadas duas configurações de requerimentos de
memória. Primeiro, foi utilizada, pelo menos em suposição, uma mesma quantidade de
memória para os dois algoritmos, justamente a quantidade mínima de memória
148
requerida pelo algoritmo de Viterbi com 2-níveis de checkpoints por diagonais com
retrocedimento restrito e particionamento móvel de memória. Segundo, a memória do
algoritmo de Viterbi com 2-níveis de checkpoints bidimensionais por linhas e colunas
com retrocedimento restrito e particionamento fixo de memória será reduzida pela
metade.
Esperou-se mostrar com os experimentos, que o algoritmo de Viterbi com 2-níveis de
checkpoints bidimensionais por linhas e colunas com retrocedimento restrito e
particionamento fixo de memória seja mais rápido, também em aplicações práticas, que
o algoritmo de Viterbi com checkpoints por diagonais com retrocedimento restrito e
particionamento móvel de memória tanto para o alinhamento seqüências pequenas, com
tamanhos médios de cerca de 368 resíduos (proteínas), quanto para seqüências de
comprimentos variando de 2.000 a centenas de milhares de resíduos (DNAs,
cromossomas, genomas).
Foram realizados três conjuntos de experimentos com esses algoritmos, nos quais são
considerados os tempos de CPU e os tempos totais de execução (wall time). No
primeiro experimento foi considerado um HMM com 2.000 nós e seqüências de de
comprimentos crescentes, dados por uma progressão aritmética com razão 2.000,
iniciando-se com uma seqüência de 2.000 resíduos. No segundo experimento foi
considerado um HMM com 2.000 nós e seqüências com comprimentos crescentes,
dados por uma progressão geométrica de razão 2, iniciando-se com uma seqüência de
2.000 resíduos. No terceiro experimento foi considerado um HMM com 368 nós e
seqüências com comprimentos crescentes de 100 a 1000 símbolos, dados por uma
progressão aritmética com razão 100.
Pode ser considerado a possibilidade do encadeamento ou concatenação de várias
seqüências de proteínas para serem comparadas de uma única vez a um HMM,
formando assim uma seqüência grande de resíduos de aminoácidos. Desta forma,
unicamente para facilitar e simplificar a programação e a experimentação foi
considerado nesses experimentos somente o alfabeto de aminoácidos, contendo 20
símbolos. Os dados usados como amostras nesses experimentos foram gerados
149
aleatoriamente com um alfabeto composto de 20 símbolos, de forma semelhante àqueles
dados dos experimentos efetuados por Powell et al. (1999). As strings para o
treinamento dos HMMs foram geradas da seguinte forma: primeiro uma string A foi
gerada aleatoriamente com probabilidades dadas pelas freqüências f(i) (freqüências de
Dayhoff) de occorrência dos 20 aminoácidos (função massa de probabilidade para
aminoácidos) obtidas do Banco de Dados SwissProt 34, considerando 21.210.388
raidicais (Eddy, 2003) e, então, as demais strings, as strings Bs, foram geradas de A
com probabilidades fixas de 0,2 para mudança, de 0,1 para inserção e de 0,1 para
remoção (serão permitidas discordâncias para o mesmo caractere).
As strings usadas nos experimentos para alinhamento contra os HMMs foram geradas
aleatoriamente com probabilidades dadas pelas freqüências f(i) (freqüências de
Dayhoff) de occorrência dos 20 aminoácidos (função massa de probabilidade para
aminoácidos) obtidas do Banco de Dados SwissProt 34, considerando 21.210.388
raidicais (Eddy, 2003).
Foi realizado, também, uma simulação do comportamento assintótico dos requerimentos
de espaço dos algoritmos considerados nos conjuntos de experimentos 1 e 2.
Os experimentos deste trabalho foram realizados num PC rodando o sistema
operacional Linux Slackware 9.1, sob o ambiente KDE 3.1, com processador Atlhom
K7 de 800 MHz , 512 Mb de RAM, 256 Kb de cache L2 e 64 Kb de cache L1 – 32 Kb
para dados e 32 Kb para instruções, e com espaço reservado para swap limitado em
2.048 Mb. Os programas foram escritos em C ANSI e compilados usando a versão
3.2.2 do Compilador GCC da GNU.
5.3 Implementação dos Algoritmos e Realização dos Experimentos
Para uma melhor discriminação na comparação entre os dois algoritmos de interesse
primário nos experimentos foram considerados duas configurações de uso de memória
para um deles. Esses algoritmos são: o algoritmo de Viterbi com 2-níveis de checkpoints
por diagonais, com retrocedimento restrito e particionamento móvel de memória; e o
algoritmo proposto nesse trabalho, o de Viterbi com 2-níveis de checkpoints
150
bidimensionais por linhas e colunas com retrocedimento restrito e particionamento fixo
de memória. O algoritmo proposto foi testado considerando-se duas quantidades de
alocação de memória para as principais estruturas de dados do algoritmo. Primeiro,
alocando-se a quantidade mínima de memória requerido pelo algoritmo de Viterbi com
2-níveis de checkpoints por diagonais, com retrocedimento restrito e particionamento
móvel de memória. Segundo, alocando-se somente a metade desta memória.
Nesse ponto, é importante adotar uma convenção para facilitar a distinção entre as duas
configurações de uso de memória do algoritmo. O algoritmo proposto com
requerimentos de memória idêntico ao algoritmo de Viterbi com 2-níveis de checkpoints
por diagonais, com retrocedimento restrito e particionamento móvel de memória, será
denominado algoritmo proposto 1 ou algoritmo com 2-níveis de checkpoints
bidimensionais 1. O algoritmo proposto com requerimentos de memória reduzido pela
metade será denominado algoritmo proposto 2 ou algoritmo com 2-níveis de
checkpoints bidimensionais 2.
É importante lembrar que existem 20 aminoácidos conhecidos, que são comuns em
organismos vivos, dos quais as proteínas são construídas. Para representá-los de forma
reduzida, tem sido utilizado dois códigos, um com uma letra e outro com três letras
(abreviatura), veja a Tabela 5.1 a seguir. Esses códigos constituem o alfabeto ou
conjunto de símbolos de emissão para os perfis-HMMs, quando eles estão modelando
seqüências de aminoácidos (proteínas).
Esse conjunto de símbolos ou alfabeto composto pelos símbolos dos aminoácidos,
Σ=A,C,D,E,F,G,H,I,K,L,M,N,P,Q,R,S,T,V,W,Y, pode ser estendido formando um
alfabeto degenerado para as seqüências de aminoácidos. Ao conjunto ou alfabeto Σ são
acrescentados os símbolos B,U,X,Z,*,-, produzindo o alfabeto degenerado para as
proteínas, Σd=A,C,D,E,F,G,H,I,K,L,M,N,P,Q,R,S,T,V,W,Y ∪ B,U,X,Z,*,-, onde:
B = (D ou N), U = seleno-cisteína (selenocysteine), X = (radical de aminoácido
desconhecido ou indeterminado), Z = (E ou Q), * = interrupção de tradução, e – (ou -) =
lacuna (gap) de tamanho indeterminado.
151
TABELA 5.1 – Códigos dos 20 Aminoácidos, com Uma e Três Letras.
Nome Completo Código Abreviatura Ácido Aspártico (Aspartate) D Asp Ácido Glutâmico (Glutamate) E Glu Alanina A Ala Arginina R Arg Asparagina N Asn Cisteína C Cys Fenilalanina F Fen Glicina G Gli Glutamina Q Gln Histidina H His Isoleucina I Ile Leucina L Leu Lisina K Lis Metionina M Met Prolina P Pro Serina S Ser Tirosina Y Tir Treonina T Tre Triptofano W Trp Valina V Val
A pontuação (o escore) de um alinhamento de uma dada seqüência a um HMM é
efetuada através da atribuição de escores em log-odds, do Modelo M, relativos a um
Modelo R – Modelo Nulo ou Aleatório – com composição aleatória da seqüência
observada. Esse modelo nulo é especificado também como um modelo totalmente
probabilístico, conforme a Figura 5.2. O estado N tem uma distribuição de
probabilidade de emissão de símbolos do alfabeto Σ, composto pelos símbolos de uma
letra dos 20 aminoácidos comuns nas proteínas dos seres vivos, dada pela freqüência
destes aminoácidos no Banco de Dados SWISSPROT 34 contendo 21.210.388 resíduos
(Eddy, 2003), veja a Tabela 5.2, mas pode ser definida, de forma alternativa, como
sendo uniformemente distribuída com probabilidade 05,0201
= . O estado E é um estado
final, silencioso, que não emite nenhum símbolo, responsável pelo encerramento da
emissão de símbolos do Modelo R. A probabilidade de transição N → N controla o
152
tamanho esperado das seqüências observadas, mas na prática ela é tão próxima de 1 que
o efeito sobre o tamanho é muito pequeno.
FIGURA 5.2 - Modelo Nulo, R, totalmente probabilístico.
TABELA 5.2 – Freqüências Dayhoff de Ocorrências de Aminoácidos do Banco de Dados Swissprot 34.
Código do Aminoácido Freqüência Relativa Observada A 0,075520 C 0,016973 D 0,053029 E 0,063204 F 0,040762 G 0,068448 H 0,022406 I 0,057284 K 0,059398 L 0,093399 M 0,023569 N 0,045293 P 0,049262 Q 0,040231 R 0,051573 S 0,072214 T 0,057454 V 0,065252 W 0,012513 Y 0,031985
FONTE: Eddy (2003).
A versão log-odds do algoritmo de Viterbi, apresentada na Seção 2.2.4, é baseada em
uma arquitetura básica para perfis-HMMs, conforme a Figura 2.2, e apresenta transições
entre os estados insert e os estados delete, entretanto essas transições são bastante
improváveis e podem ser suprimidas quando não estão presentes nos modelos
construídos. Tipicamente, assume-se que a distribuição das probabilidades de emissão
dos estados Ij é a mesma distribuição do modelo nulo. Quando usado com escores em
153
log-odds essas probabilidades se anulam produzindo um escore nulo:
01log)(
log ==t
j
O
tI
q
Oe. Entretanto, em termos de implementação essa suposição, apesar
de proporcionar uma redução das operações a serem realizadas a cada iteração,
melhorando o desempenho do algoritmo, implicaria num algoritmo de uso limitado a
essas instâncias particulares das versões log-odds dos algoritmos de Viterbi desenhados
especialmente para perfis-HMMs. Desta forma, considerando que a adoção ou não desta
suposição provoca impactos equivalentes em todas as versões dos algoritmos de Viterbi
que são alvos dos experimentos previstos nesse trabalho, optou-se por construir um
algoritmo de uso mais geral, que em termos práticos mostra-se ser mais útil.
As versões log-odds dos algoritmos de PD de Viterbi considerados nos experimentos
são detalhados a seguir, explicitando-se melhor as inicializações das variáveis e todas as
condições de contorno, incluindo a fase de retrocedimento e as mudanças necessárias
para essa fase. Seja V o escore em log-odds do melhor caminho concordando
(matching) a subseqüência O
)(tMj
1 ... i ao sub-modelo até o estado j, terminando com Oi
sendo emitido pelo estado Mj. Similarmente, seja V o escore do melhor caminho
terminando com O
)(tIj
i sendo emitido pelo estado Ij. Seja, de forma análoga, V o escore
para o melhor caminho terminando no estado D
)(tDj
j. Também é necessário um array
para guardar, para cada t, j e ( )tTBXj MIDX ,,∈ , o argumento que maximiza V ,
e V .
)(tMj
)(tV Ij )(tD
j
algoritmo de viterbi básico para perfis-HMMs (usando log-odds)
1) Inicialização:
154
( ) ( )( ) ( )( ) ( )
( ) ( ) ( )( ) ( ) ( )( ) ( ) ( )( ) ( ) ( )( ) ( )
( ) ( ) NjTBVNjTBV
NjDDTBaVVTBaVV
TtIItTBatVtVTBaVV
TttTBtVTttTBtV
TBV
j
j
jjj
IIj
MMj
DDDDj
Dj
DDMMD
IIIII
IIMMI
MM
DD
MM
≤≤=−∞=
≤≤=−∞=
≤≤=+==+=
≤≤=+−==+=
≤≤=−∞=≤≤=−∞=
==
−−
1 para ,100 ,01 para ,100 ,0
2 para ,0 ,log0090 ,log00
2 para , ,log191 ,log01
1 para ,10 ,1 para ,10 ,
100 ,00
1
110
000
000
0
0
0
1
01
00
00
0
0
0
2) Fase de computações pra frente (forward). Computa e salva a matriz de escores
de Viterbi e as decisões tomadas a cada passo nessa fase. A computação é
efetuada conforme as seguintes equações, para TtNj ≤≤≤≤ 1 e 1 .
( ) [ ]
( ) [ ]
( ) [ ]
∈=
+
+
+
=
∈=
+−
+−
+−
+=
∈=
+−
+−
+−
+=
−
−
−
−
−
−
−
−
−
−
−
−
DDIDMDkktVtTBatVatVatV
tV
DIIIMIkktVtTBatVatVatV
q
OetV
DMIMMMkktVtTBatVatVatV
q
OetV
Dj
kD
DDDj
DIIj
DMMj
Dj
Ij
kI
IDDj
IIIj
IMMj
O
tIIj
Mj
kM
MDDj
MIIj
MMMj
O
tMMj
j
jj
jj
jj
j
jj
jj
jj
t
j
j
jj
jj
jj
t
j
,,: transiçãouma é ,)(maxarglog)(log)(log)(
max)(
,,: transiçãouma é ,)(maxarg
log)1(log)1(log)1(
max)(
log)(
,,: transiçãouma é ,)(maxarglog)1(log)1(log)1(
max)(
log)(
1
1
1
1
1
1
1
1
1
1
1
1
3) Encerramento da fase de computações pra frente (forward).
155
[ ]
∈+=
+++
=+
++
+
+
+
+
DMIMMMkkTVq
aTVaTVaTV
TV
MN
kT
MDD
N
MII
N
MMM
NM
N
NN
NN
NN
,,: transiçãouma é ,)1(maxarg
log)(log)(log)(
max)1(
1*
1
1
1
1
1
4) Fase de retrocedimento sobre a matriz de escores de Viterbi.
( )
∈
−−==+
9,,,,,,,,,e 1,,2,1, onde ,
*
**
1
IIIMIDMIMMMDDIDMDDqTTTttTBq
t
qtt
K
algoritmo de viterbi com 2-níveis de checkpoints por linhas e particionamento
móvel de memória, para perfis-hmms (usando log-odds)
1) Inicialização.
• ; ( ) 000 =MV
2) Fase de computações pra frente (forward).
• 1 ; ;0 −←←← cpcp RkRiendibegin ;
• Computa iterativamente, para cpRk <≤0 , TtNj ≤≤≤≤ 0 e 0
• Computa e salva as linhas ibegin a iend da matriz de Viterbi.
• Salva as decisões tomadas na computação das células destas linhas.
• Salva como um checkpoint(k) a linha iend e as decisões tomadas em cada
célula desta linha.
• ( ) ; ;1 ; ; −−−−+←←← kauxiendiendiendiendibeginibeginaux
• Término do loop (j = N).
• Encerramento da fase de computações pra frente (forward).
156
[ ]
∈+=
+++
=+
++
+
+
+
+
DMIMMMkkTVq
aTVaTVaTV
TV
MN
kT
MDD
N
MII
N
MMM
NM
N
NN
NN
NN
,,: transiçãouma é ,)1(maxarg
log)(log)(log)(
max)1(
1*
1
1
1
1
1
3) Fase de retrocedimento sobre a matriz de escores de Viterbi.
• Executa o retrocedimento sobre as linhas N e N-1, correspondente aos
checkpoints 0 e 1.
• Re-computa iterativamente as seções da matriz entre dois checkpoints
consecutivos e executa o retrocedimento em cada uma destas seções. Assim,
para cpRk <≤2 , TtNj ≤≤−≤≤ 0 e 21 , faz-se:
• Re-computa e salva as linhas da seção k da matriz de Viterbi.
• Salva as decisões tomadas na re-computação das células destas linhas.
• Executa o retrocedimento nessa seção k.
• ;1−← kk
• Término (j = 0).
algoritmo de viterbi com 2-níveis de checkpoints por diagonais, com retrocedimento
restrito e particionamento móvel de memória, para perfis-HMMs (usando log-
odds)
1) Inicialização.
• ; ( ) 000 =MV
2) Fase de computações pra frente (forward).
• 1 ; ;0 −←←← cpcp RkRiendibegin ;
157
• Computa iterativamente, para cpRk <≤0 , ( ) TtTNj ≤≤++≤≤ 0 e 10
• Computa e salva as diagonais ibegin a iend da matriz de Viterbi.
• Salva as decisões tomadas na computação das células destas diagonais.
• Salva como um checkpoint(k) a diagonal iend e as decisões tomadas em
cada célula desta diagonal.
• ( ) ; ;1 ; ; −−−−+←←← kauxiendiendiendiendibeginibeginaux
• Término do loop (j = N+T+1).
• Encerramento da fase de computações pra frente (forward).
[ ]
∈+=
+++
=+
++
+
+
+
+
DMIMMMkkTVq
aTVaTVaTV
TV
MN
kT
MDD
N
MII
N
MMM
NM
N
NN
NN
NN
,,: transiçãouma é ,)1(maxarg
log)(log)(log)(
max)1(
1*
1
1
1
1
1
3) Fase de retrocedimento sobre a matriz de escores de Viterbi.
• Executa o retrocedimento sobre as diagonais N+T+1 e N+T, correspondentes
aos checkpoints 0 e 1.
• Re-computa iterativamente as seções da matriz entre dois checkpoints
consecutivos e executa o retrocedimento em cada uma destas seções. Essa
re-computação é limitada (retrocedimento restrito) recomputando-se somente
a região entre dois checkpoints consecutivos limitada pelos pontos ,
e , onde
( )ji,
( )cji −, ( jci ,− ) ( )ji, é o ponto onde o alinhamento ótimo atingiu a
seção atual na iteração anterior. Assim, para cpRk <≤2 , 1 e
.
1−T+≤≤ Nj
Tt ≤≤0
• Re-computa e salva as diagonais da seção k da matriz de Viterbi.
158
• Salva as decisões tomadas na re-computação das células destas
diagonais.
• Executa o retrocedimento nessa seção k.
• ;1−← kk
• Término (j = 0).
algoritmo de viterbi com 2-níveis de checkpoints bidimensionais, com
retrocedimento restrito e particionamento fixo de memória, para perfis-HMMs
(usando log-odds)
1) Inicialização.
• ; ( ) 000 =MV
2) Fase de computações pra frente (forward).
• Computa a linha 0 (zero).
• Salva a linha 0 (zero) no checkpoint 0 (zero).
• Salva as decisões tomadas a cada passo da computação da linha 0 (zero).
• Computa iterativamente, para TtNj ≤≤≤≤ 0 e 1 , as linhas da matriz
de Viterbi, usando 2 linhas de memória. Salva os checkpoints
bidimensionais.
• Computa e salva a linha j da matriz de Viterbi.
• Salva as decisões tomadas na computação das células da linha j.
• Salva como checkpoints as linhas 1 (ondelbdll Rcc ,,2, K
nncl 2
= ) e as
decisões tomadas em cada célula destas linhas.
159
• Salva como checkpoints as células das colunas 1 .
(onde
cbdcc Rcc ,,2, K
mmcc 2
= ) e as decisões tomadas em cada célula destas células.
• Término do loop (j = N).
• Encerramento da fase de computações pra frente (forward).
[ ]
∈+=
+++
=+
++
+
+
+
+
DMIMMMkkTVq
aTVaTVaTV
TV
MN
kT
MDD
N
MII
N
MMM
NM
N
NN
NN
NN
,,: transiçãouma é ,)1(maxarg
log)(log)(log)(
max)1(
1*
1
1
1
1
1
3) Fase de retrocedimento sobre a matriz de escores de Viterbi.
• Re-computa a última seção da matriz de Viterbi – aquela situada no canto
inferior direito da matriz e delimitada pelos checkpoints (linha) e
(coluna) e a última linha e coluna da matriz . Executa o retrocedimento sobre
essa seção da matriz.
lbdRcbdR
• Iterativamente, escolhe a seção adjacente à ultima seção re-computada,
aquela que o caminho de estados recuperado pelo procedimento de
retrocedimento atingiu, até que seja alcançada a primeira seção da matriz de
Viterbi – aquela situada no canto superior esquerdo da matriz e delimitada
pelos checkpoints (linha) e c (coluna) e a primeira linha e coluna da
matriz.
lc c
• Re-computa as células de PD necessárias para o retrocedimento em cada
uma destas seções da matriz de Viterbi. Essas re-computações são
limitadas ou restringidas (retrocedimento restrito) pela linha ou coluna
do ponto onde o caminho ótimo, recuperado, atingiu a seção atual
na iteração anterior. Se a seção atual foi atingida na linha i
( ji, )
160
(correspondendo ao checkpoint ), então os cálculos são limitados pela
coluna j, se foi atingida na coluna j (correspondendo ao checkpoint c ),
então são limitados pela linha i.
lc
c
• Salva as decisões tomadas na re-computação destas células.
• Executa o retrocedimento em cada uma destas seções.
• Término (j = 0).
Nos experimentos foram usados dois perfis-HMMs, um com 2.000 nós e outro com 368
nós, com arquitetura linear e conexões básicas entre os nós, conforme o modelo
apresentado e descrito na Seção 2.2.4. Eles foram treinados usando conjuntos de
seqüências geradas aleatoriamente com base no alfabeto de aminoácidos Σ, composto de
20 símbolos, de forma semelhante àqueles dados usados nos experimentos efetuados por
Powell et al. (1999).
Os perfis-HMMs foram treinados usando o pacote de software HMMpro versão 2.2.2
(B47). O HMMpro é um simulador de HMMs, de propósito geral, que pode ser
utilizado para mineração, análise e modelagem de informações biológicas geradas pelos
projetos de estudos de genomas ou por outros projetos (Net-ID, 2003).
5.4 Apresentação e Análise dos Resultados da Experimentação
Antes de iniciar a análise dos conjuntos de testes dos dois experimentos previstos é
importante que se faça uma análise da estimativa do comportamento de requerimentos
de memória dos algoritmos. Essas estimativas foram confeccionadas com base na
memória necessária para acomodar as principais estruturas de dados dos algoritmos, que
são a matriz de PD, no caso do algoritmo básico, e o arrays para armazenar os
checkpoints, no caso dos demais algoritmos. Foram considerados tamanhos de
seqüências variando de 2.000 a 2.048.000 resíduos com incrementos segundo uma
distribuição geométrica de razão 2. É importante salientar que essas estimativas, além
de desprezar as outras estruturas de dados alocadas pelo aplicativo (algoritmo), não
161
considera a memória ocupada pelo Sistema Operacional e pelo ambiente gráfico KDE,
mas essas têm o mesmo impacto em todos os aplicativos (algoritmos).
As Tabelas 5.3 e 5.4 sintetizam os dados destas estimativas, sendo que na Tabela 5.3
estão relatados os dados brutos, com valores absolutos expressos em Mega bytes (Mb).
A Tabela 5.4 contém as estimativas da Tabela 5.3 normalizados pelos valores do
algoritmo básico. O algoritmo básico foi tomado como referência da normalização,
ficando, portanto, com um valor de 1 unidade.
O Sistema Operacional precisou alocar pelo menos 1.099 Mb para o algoritmo de
Viterbi básico alinhar seqüências de 32.000 resíduos, relativas à memória para
armazenagem das principais estruturas de dados, entretanto não foi possível alocar
2.197 Mb para o alinhamento de seqüências com 64.000 resíduos. O mesmo aconteceu
com o algoritmo de Viterbi com 2-níveis de checkpoints por diagonais, que só
conseguiu memória suficiente para alinhar seqüencias de até 64.000 resíduos, mas não
conseguiu alocar 1.584 Mb – memória para as principais estruturas de dados – para
alinhar seqüências com 128.000 resíduos.
Os demais algoritmos de Viterbi foram testados contra seqüências de até 512.000
resíduos, mas observando os dados da Tabela 5.3, sabendo-se que foi possível alocar
1.099 Mb para o algoritmo de Viterbi básico e supondo tamanhos crescentes de
seqüências conforme uma progressão geométrica com razão 2, espera-se que seja
possível alinhar seqüências com 1.024.000 resíduos com o algoritmo de Viterbi com 2-
níveis de checkpoints por linhas, que necessita de pelo menos 1.107 Mb, e com o
algoritmo de Viterbi proposto 2, necessita de pelo menos 808 Mb.
Observando-se os dados da Tabela 5.4 pode-se verificar o comportamento assintótico, a
posteriori, dos requisitos de memória dos algoritmos considerados nos experimentos,
quando comparados com o algoritmo de Viterbi básico. O algoritmo de Viterbi com 2-
níveis de checkpoints por linhas requer 3,2% de memória para todos os tamanhos de
seqüências. Os algoritmos de Viterbi propostos 1 e 2 apresentam queda nos requisitos de
memória para tamanhos crescentes de seqüências, sendo que o proposto 1 decresce de
8,8% (2.000 resíduos) para 4,5% (2.048.000 resíduos) e o proposto 2 decresce de 4,4%
162
(2.000 resíduos) para 2,3% (2.048.000 resíduos). Já o algoritmo de Viterbi com 2-níveis
de checkpoints por diagonais apresenta comportamento crescente dos requisitos de
memória para tamanhos crescentes de seqüências, ele cresce de 6,3% (2.000 resíduos)
para 143,2% (2.048.000 resíduos).
O que se deve observar nesses algoritmos com requisitos de memória reduzido é
justamente o comportameno assintótico dos requisitos de memória destes quando o
tamanho das seqüências cresce, pois eles são úteis justamente para o alinhamento de
seqüências cujos comprimentos estão além daqueles que o algoritmo de Viterbi básico
pode tratar. Nesse sentido, o algoritmo de Viterbi com 2-níveis de checkpoints por
diagonais, que apresenta um comportamento crescente dos requisitos de memória,
chegando mesmo a ultrapassar os requisitos do algoritmo de Viterbi básico, é o que
apresentou o pior comportamento assintótico, indicando que os requisitos de memória
crescem para o infinito mais rapidamente que o algoritmo de Viterbi básico, quando o
tamanho das seqüências tendem ao infinito.
TABELA 5.3 – Resultados das Estimativas dos Requerimentos de Memória para os Algoritmos de Viterbi (Valores em Mb).
Viterbi com 2-níveis de checkpoints (por) Comp. Seqüências
Viterbi Básico Linhas Diagonais Proposto 1 Proposto 2
2.000 69 2 4 6 3 4.000 137 4 11 10 5 8.000 275 9 27 18 9 16.000 549 17 74 33 16 32.000 1.099 35 202 61 30 64.000 2.197 69 564 114 57 128.000 4.395 138 1.584 218 109 256.000 8.789 277 4.460 421 211 512.000 17.578 554 12.595 823 411
1.024.000 35.156 1.107 35.596 1.616 808 2.048.000 70.313 2.215 100.652 3.192 1.596
O algoritmo de Viterbi com 2-níveis de checkpoints por linhas apresentou um
comportamento assintótico constante dos requisitos de memória, relativamente ao
algoritmo de Viterbi básico. Os algoritmos de Viterbi com 2-níveis de checkpoints
163
propostos, 1 e 2, apresentaram comportamento assintótico decrescente dos requisitos de
memória, relativamente ao algoritmo de Viterbi básico. Mostrando-se, desta forma,
serem mais úteis do que os demais algoritmos considerados nesses experimentos,
quando a economia de espaço é a variável de performance mais importante a ser
considerada.
TABELA 5.4 – Resultados das Estimativas Normalizadas dos Requerimentos de Memória para os Algoritmos de Viterbi (Básico = 1).
Viterbi com 2-níveis de checkpoints (por) Comp. Seqüências
Viterbi Básico Linhas Diagonais Proposto 1 Proposto 2
2.000 1,000 0,032 0,063 0,088 0,044 4.000 1,000 0,032 0,077 0,076 0,038 8.000 1,000 0,032 0,100 0,066 0,033 16.000 1,000 0,032 0,134 0,060 0,030 32.000 1,000 0,032 0,184 0,055 0,028 64.000 1,000 0,032 0,257 0,052 0,026 128.000 1,000 0,032 0,361 0,050 0,025 256.000 1,000 0,032 0,508 0,048 0,024 512.000 1,000 0,032 0,717 0,047 0,023
1.024.000 1,000 0,032 1,013 0,046 0,023 2.048.000 1,000 0,032 1,432 0,045 0,023
Após a codificação, otimização e compilação dos algoritmos, foram efetuados os
conjuntos de testes previstos nos dois experimentos. O conjunto de testes previsto no
primeiro experimento foi preparado para verificar o comportamento dos algoritmos até
que os recursos de memória do PC fossem exauridos por algum dos algoritmos, ou seja,
até o início de paginação excessiva. O conjunto de testes previsto no segundo
experimento foi preparado para verificar o comportamento dos algoritmos, pelo menos,
até que os recursos de memória do PC fossem exauridos por algum dos algoritmos de
interesse primário nessa investigação, por um dos algoritmos propostos ou pelo
algoritmo de Viterbi com 2-níveis de checkpoints por diagonais, com retrocedimento
restrito e particionamento móvel de memória. Entretanto, o conjunto de testes previsto
no segundo experimento foi ampliado para verificar, também, o comportamento do
algoritmo de Viterbi com 2-níveis de checkpoints por linhas até o ponto em que esse
exaurisse os recursos de memória do PC. O conjunto de testes previsto no terceiro
164
experimento foi preparado para verificar o comportamento dos algoritmos no
alinhamento de seqüências relativamente pequenas, por exemplo as seqüências de
proteínas que possuem comprimento médio de 368 resíduos.
Nos três conjuntos de testes realizados, foram calculados os tempos de CPU e os tempos
totais de execução (wall time), para todos os algoritmos. Nesses testes foram tomadas as
médias aritméticas de 5 re-estimações do tempo de CPU e de 5 re-estimações do tempo
total de execução (Wall Time) para cada seqüência de aminoácidos (proteínas). Os
resultados do primeiro e do segundo conjunto de testes, os tempos de CPU e os tempos
totais de execução (Wall time), estão sintetizados nas Tabelas 5.5 e 5.6,
respectivamente. Esses tempos foram medidos tomando-se um segundo como a unidade
básica de medida do tempo e estão registrados nas tabelas como valores absolutos em
segundos.
TABELA 5.5 – Resultados dos Tempos de CPU e dos Tempos Totais de Execução (Wall Time) do Primeiro Experimento para os Algoritmos de Viterbi (Tempos em Segundos).
Viterbi com 2-níveis de checkpoints (por) Viterbi Básico linhas diagonais proposto 1 proposto 2Comp. seqüên. CPU Wall CPU Wall CPU Wall CPU Wall CPU Wall
2.000 1,248 1,304 2,326 2,338 2,154 2,480 1,016 1,066 1,024 1,025 4.000 2,552 2,580 4,798 4,826 5,124 5,155 2,158 2,175 2,176 2,315 6.000 3,896 3,993 7,330 7,385 8,232 8,280 3,342 3,369 3,292 3,305 8.000 6,254 6,292 11,946 12,044 11,148 11,224 5,926 5,953 5,902 5,923
10.000 7,862 7,916 14,968 15,065 14,260 14,368 7,448 7,501 7,394 7,428 12.000 22,498 76,456 16,034 18,148 17,280 17,389 9,150 9,204 8,938 8,974 14.000 22,048 113,206 21,078 21,210 20,436 20,564 10,498 10,560 10,498 10,53816.000 28,270 126,858 24,092 24,241 23,506 23,656 12,012 12,080 11,962 12,01518.000 33,548 144,600 27,078 27,243 26,474 26,626 13,492 13,569 13,406 13,46120.000 33,042 169,249 30,104 30,292 29,096 29,282 15,022 15,121 14,902 14,97522.000 38,735 189,375 33,156 33,366 32,286 32,474 16,560 16,668 16,404 16,47824.000 44,480 197,929 36,258 36,440 35,588 35,844 18,026 18,141 17,902 17,97926.000 48,318 208,825 39,362 39,639 38,596 38,820 19,556 19,681 19,496 19,58028.000 48,292 211,159 42,286 42,662 41,898 42,177 21,076 21,202 21,088 21,18630.000 59,052 223,569 45,314 45,621 44,760 45,069 22,622 22,762 22,506 22,60232.000 67,618 224,634 48,306 48,613 48,148 48,454 24,070 24,227 23,928 24,043
165
Essas medidas – os resultados – do desempenho dos algoritmos foram, também,
apresentados em dois conjuntos de gráficos, para permitir um melhor entendimento
através da visualização do comportamento destes. Foram elaborados quatro gráficos de
linhas para os dados das Tabelas 5.5 e 5.6, sendo dois para cada uma das tabelas. Os
gráficos das Figuras 5.3 e 5.4 foram confeccionados com os dados da Tabela 5.5,
representando os tempos de CPU e os tempos totais de execução, respectivamente,
enquanto que os gráficos das Figuras 5.5 e 5.6 foram confeccionados com os dados da
Tabela 5.6, representando os tempos de CPU e os tempos totais de execução,
respectivamente.
Os gráficos das Figuras 5.3 e 5.4 foram confeccionados com base nos dados
normalizados em relação aos dados do algoritmo de Viterbi básico (básico = 1). Os
gráficos das Figuras 5.5 e 5.6 foram confeccionados adotando-se a escala logarítmica
em ambos os eixos de coordenadas cartesianas, pois a amplitude dos dados é muito
grande para assumir uma escala linear e ainda assim ressaltar o comportamento da
performance dos algoritmos usados nos experimentos.
Das Tabelas 5.5 e 5.6, e das Figuras 5.3 a 5.6, pode ser verificado que: o algoritmo de
Viterbi básico provocou atividade de paginação significativa para o alinhamento de
seqüências de tamanhos maiores ou iguais a 12.000 resíduos e provocou o início de
atividade excessiva de paginação para seqüências compostas por 32.000 resíduos; o
algoritmo de Viterbi com 2-níveis de checkpoints por diagonais provocou o início da
atividade significativa de paginação para o alinhamento de seqüências de tamanhos
compreendidos entre 32.000 e 64.000 resíduos e provocou atividade excessiva de
paginação para seqüências compostas por 64.000 resíduos; o algoritmo de Viterbi com
2-níveis de checkpoints por linhas provocou o início da atividade significativa de
paginação para o alinhamento de seqüências de tamanhos compreendidos entre 256.000
e 512.000 resíduos e provocou atividade excessiva de paginação para seqüências
compostas por 512.000 resíduos.
Por outro lado, pode-se observar que os algoritmos de Viterbi propostos 1 e 2 somente
apresentaram pequenos aumentos nos tempos totais de execução para seqüências
166
compostas por 512.000 resíduos, quando comparados com os respectivos tempos de
CPU. O algoritmo de Viterbi proposto 1 apresentou um aumento de aproximadamente
30%, o que talvez possa indicar proximidade do início de atividade significativa de
paginação. Já, o algoritmo de Viterbi proposto 2 apresentou um aumento de
aproximadamente 10%, o que ainda não chega a ser uma sinalização do início de
atividade significativa de paginação. Assim, o algoritmo de Viterbi proposto 1 ainda
pode alinhar seqüências bem maiores do que 512.000 resíduos (no PC usado para os
testes deste trabalho) e, o algoritmo de Viterbi proposto 2 ainda pode alinhar seqüências
maiores do que 1.024.000 resíduos no PC usado para os testes deste trabalho,
requerendo alocação de pelo menos 808 Mb de memória (Tabela 5.3).
TABELA 5.6 – Resultados dos Tempos de CPU e dos Tempos Totais de Execução (Wall Time) do Segundo Experimento para os Algoritmos de Viterbi (Tempos Em Segundos).
Viterbi com 2-níveis de checkpoints (por) Viterbi Básico linhas diagonais proposto 1 proposto 2Comp.
seqüên. CPU Wall CPU Wall CPU Wall CPU Wall CPU Wall2.000 1,248 1,304 2,326 2,338 2,154 2,480 1,016 1,066 1,024 1,025 4.000 2,552 2,580 4,798 4,826 5,124 5,155 2,158 2,175 2,176 2,315 8.000 6,254 6,292 11,946 12,044 11,148 11,224 5,926 5,953 5,902 5,923 16.000 28,270 126,858 24,092 24,241 23,506 23,656 12,012 12,080 11,962 12,015 32.000 67,618 224,634 48,306 48,613 48,148 48,454 24,070 24,227 23,928 24,043 64.000 - - 97,962 98,373 104,618 640,054 48,676 48,876 48,240 48,469
128.000 - - 196,884 197,777 - - 97,500 97,938 96,980 97,427 256.000 - - 393,384 396,260 - - 206,320 253,470 193,026 194,222512.000 - - 1924,024 6729,183 - - 436,422 559,476 423,274 468,180
Os algoritmos propostos 1 e 2 tiveram desempenhos semelhantes em relação aos tempos
de CPU e tempos totais de execução no alinhamento de seqüências no intervalo de
2.000 a 32.000 resíduos a um HMM com 2.000 nós, veja a Tabela 5.5 e as Figuras 5.3 e
5.4. Com relação aos tempos de CPU o algoritmo proposto 2 foi, na maioria das vezes,
mais rápido do que o 1, chegando a uma diferença máxima de 2.5% (6.000 resíduos),
sendo mais lento, somente, para as seqüências com 4.000 (0,8%) e 28.000 (0.06%)
resíduos. Com relação aos tempos totais de execução o algoritmo proposto 2 foi mais
167
lento do que o 1 apenas para seqüências de 4.000 resíduos (6%) e, foi no máximo 2.5%
mais rápido (12.000 resíduos) para as demais seqüências.
Observando-se a Tabela 5.5 e as Figuras 5.3 e 5.4, pode-se notar que os algoritmos
propostos 1 e 2 sempre apresentaram desempenho superior aos demais para as medidas
de tempos no alinhamento de seqüências no intervalo de 2.000 a 32.000 a um HMM
com 2.000 nós, sendo no mínimo 5,2% (8.000 e 10.000 resíduos) mais rápido que o
algoritmo de Viterbi básico para as seqüências no intervalo considerado. Os algoritmos
de Viterbi propostos 1 e 2 foram com relação aos tempos de CPU, pelo menos 2,1 (em
14.000 resíduos) vezes mais rápidos, e com relação aos tempos totais de execução cerca
de 8 a 10 vezes mais rápidos do que o algoritmo de Viterbi básico para seqüências
maiores ou iguais a 12.000 resíduos – início da degradação do uso de memória pelo
algoritmo de Viterbi básico.
0,00
0,50
1,00
1,50
2,00
2,50
2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32
Tamanhos das seqüência (x 1000 radicais)
Tem
pos d
e C
PU n
orm
aliz
ados
(bás
ico
= 1)
Viterbi Básico
2-níveis Checkpoints Linha
2-níveis Checkpoints Bidimensional 1
2-níveis Checkpoints Bidimensional 2
2-níveis Checkpoints Diagonais
FIGURA 5.3 – Tempos de CPU dos algoritmos Viterbi para alinhamento de seqüências de aminoácidos a um HMM com 2000 nós.
168
O algoritmo de Viterbi com 2-níveis de checkpoints por linhas e o por diagonais
desempenharam de forma semelhante no intervalo de 2.000 a 32.000, veja a Tabela 5.5
e as Figuras 5.3 e 5.4, com discrepâncias significativas no intervalo de 2.000 a 10.000
resíduos, mas com desempenho praticamente semelhante para seqüências no intervalo
de 12.000 a 32.000 resíduos.
0,00
0,50
1,00
1,50
2,00
2,50
2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32
Tamanhos das Seqüência (x 1000 radicais)
Tem
pos t
otai
s de
exec
ução
nor
mal
izad
os (b
ásic
o =
1)
Viterbi Básico
2-níveis Checkpoints Linha
2-níveis Checkpoints Bidimensional 1
2-níveis Checkpoints Bidimensional 2
2-níveis Checkpoints Diagonais
FIGURA 5.4 – Tempos totais de execução dos algoritmos de Viterbi para alinhamentos de seqüências de aminoácidos a um HMM com 2000 nós.
Da Tabela 5.5 e da Figura 5.3 pode-se notar que o algoritmo de Viterbi com 2-níveis de
checkpoints por linhas e o por diagonais sempre apresentaou desempenho inferior aos
algoritmos propostos 1 e 2 para as medidas de tempos no alinhamento de seqüências no
intervalo de 2.000 a 32.000 resíduos a um HMM com 2.000 nós. Os algoritmos de
Viterbi propostos 1 e 2 foram, respectivamente, cerca de 1,7 a 2,5 vezes mais rápidos,
em tempos de CPU, do que o algoritmo de Viterbi com 2-níveis de checkpoints por
linhas e do que o algoritmo por diagonais. Essas comparações são muito semelhantes
para os tempos totais de execução, veja a Figura 5.4, com os algoritmos propostos 1 e 2
169
sendo cerca de 1,9 a 2,5 vezes mais rápidos que o algoritmo de Viterbi com 2-níveis de
checkpoints por linhas e do que o algoritmo por diagonais.
O algoritmo de Viterbi com 2-níveis de checkpoints por linhas e o por diagonais
apresentaram desempenho inferior ao algoritmo de Viterbi básico para as medidas de
tempos no intervalo de 2.000 a 10.000 resíduos, com queda de desempenho por um
fator de ~1,8, veja a Tabela 5.5 e as Figuras 5.3 e 5.4. Por outro lado para seqüências no
intervalo de 14.000 a 32.000 resíduos eles apresentaram um desempenho superior ao
algoritmo de Viterbi básico para as medidas de tempos, com um ganho de desempenho
de até ~1,4 para os tempos de CPU e de até ~5,0 para os tempos totais de execução.
1
10
100
1000
10000
2 4 8 16 32 64128
256512
Tamanhos das seqüências (x 1000 radicais)
Tem
pos
de C
PU (s
egun
dos)
Viterbi Básico2-níveis Checkpoints Linha2-níveis Checkpoints Bidimensional 12-níveis Checkpoints Bidimensional 22-níveis Checkpoints Diagonal
FIGURA 5.5 – Tempos de CPU dos algoritmos de Viterbi para alinhamentos de seqüências de aminoácidos a um HMM com 2000 nós (eixos logarítmicos).
Os dados sintetizados na Tabela 5.6 e nas Figuras 5.5 e 5.6, apresentam resultados
relativos aos tempos de CPU e tempos totais de execução semelhantes aos da Tabela 5.5
e das Figuras 5.3 e 5.4, exceto para os pontos em que o algoritmo de Viterbi com 2-
170
níveis de checkpoints por diagonais e o algoritmo por linhas exauriram os recursos de
memória do PC. O algoritmo de Viterbi com 2-níveis de checkpoints por diagonais
provocou atividade excessiva de paginação para seqüências de 64.000 resíduos e não
conseguiu alocar recursos de memória para alinhar seqüências com 128.000 resíduos,
veja nas Figuras 5.5 e 5.6, enquanto que o algoritmo de Viterbi com 2-níveis de
checkpoints por linhas provocou atividade excessiva de paginação para seqüências de
512.000 resíduos, veja nas Figuras 5.5 e 5.6.
Os algoritmos propostos 1 e 2 tiveram desempenhos semelhantes em relação aos tempos
de CPU e tempos totais de execução no intervalo de 2.000 a 512.000 resíduos, veja a
Tabela 5.6 e as Figuras 5.5 e 5.6. Enquanto, o algoritmo de Viterbi com 2-níveis de
checkpoints por diagonais e o algoritmo por linhas, também, desempenharam de forma
semelhante até o ponto em que o algoritmo por diagonais exauriu os recursos de
memória do PC, ou seja, para seqüências no intervalo de 2.000 a 32.000 resíduos.
1
10
100
1000
10000
2 4 8 16 32 64 128
256
512
Tamanhos das seqüências (x 1000 radicais)
Tem
pos
tota
is d
e ex
ecuç
ão (s
egun
dos)
Viterbi Básico2-níveis Checkpoints Linha2-níveis Checkpoints Bidimensional 12-níveis Checkpoints Bidimensional 22-níveis Checkpoints Diagonal
FIGURA 5.6 – Tempos totais de execução dos algoritmos de Viterbi para alinhamentos de seqüências de aminoácidos a um HMM com 2000 nós (eixos logarítmicos).
171
O algoritmo de Viterbi com 2-níveis de checkpoints por linhas e os algoritmos propostos
1 e 2 tiveram comportamento bastante semelhante no intervalo considerado, até que o
algoritmo por linhas exaurisse os recursos de memória do PC, o que ocorreu para
seqüências com 512.000 resíduos, veja a Tabela 5.6 e as Figuras 5.5 e 5.6. Pode-se notar
que os algoritmos propostos 1 e 2 foram ~2 vezes mais rápidos do que o algoritmo de
Viterbi com 2-níveis de checkpoints por linhas, no intervalo correspondendo a
seqüências com 2.000 a 256.000 resíduos.
0,00
10,00
20,00
30,00
40,00
50,00
60,00
70,00
80,00
2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32
Tamanhos das seqüência (x 1000 radicais)
Tem
pos d
e C
PU (s
egun
dos)
Viterbi Básico2-níveis Checkpoints Linha2-níveis Checkpoints Bidimensional 12-níveis Checkpoints Bidimensional 22-níveis Checkpoints Diagonais
FIGURA 5.7 – Tempos de CPU dos algoritmos Viterbi para alinhamento de seqüências de aminoácidos a um HMM com 2000 nós.
Nota-se, também, observando as Figuras 5.3, 5.4, 5.5 e 5.6, que os algoritmos propostos
1 e 2 tiveram desempenhos similares no alinhamento de seqüências a um HMM com
2.000 nós, em todo o intervalo considerado, quase sempre com uma leve vantagem em
desempenho para o algoritmo proposto 2. É importante salientar que esse ganho em
desempenho, mesmo que por um fator muito pequeno é uma informação significativa,
pois o algoritmo proposto 2 usa menos memória e efetua mais re-computações que o
algoritmo proposto 1 e, logo, deveria sofrer uma queda de desempenho maior. Talvez
172
isto se deva ao fato que o algoritmo proposto 2 divida a matriz de PD em um número 4
vezes menor de seções do que o algoritmo proposto 1 e, portanto, tire mais proveito do
retrocedimento restrito. O que ressalta o comportamento, até intuitivo, do uso de
procedimentos de retrocedimento restrito nesses algoritmos com checkpoints é que: o
aumento da memória disponível para uma determinada instância do problema provoca
uma redução no ganho de performance adicional devido ao uso do retrocedimento
restrito, ou alternativamente, a redução da memória disponível para uma determinada
instância do problema provoca um aumento no ganho de performance adicional devido
ao uso do retrocedimento restrito.
0,00
50,00
100,00
150,00
200,00
250,00
2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32
Tamanhos das seqüência (x 1000 radicais)
Tem
pso
tota
is d
e ex
ecuç
ão (s
egun
dos)
Viterbi Básico2-níveis Checkpoints Linha2-níveis Checkpoints Bidimensional 12-níveis Checkpoints Bidimensional 22-níveis Checkpoints Diagonais
FIGURA 5.8 – Tempos totais de execução dos algoritmos Viterbi para alinhamento de seqüências de aminoácidos a um HMM com 2000 nós.
É importante comparar esses resultados com aqueles obtidos nos experimentos de
Tarnas e Hughey (1998), para análise do comportamento de algoritmos de programação
dinâmica usados na computação de medidas de interesse em HMMs. Nesses
experimentos seqüências de aminoácidos menores do que ou iguais a 10.000 resíduos
são utilizadas em computações com o algoritmo de Viterbi, ambos com e sem
173
checkpoints. O uso significativo de memória virtual pelos algoritmos de programação
dinâmica básicos inicia-se para seqüências maiores do que 7.000 resíduos e tornam-se
críticos para seqüências maiores do que 8.000 resíduos. Eles consideram o algoritmo de
Viterbi básico e o com 2-níveis de checkpoints por diagonais e retrocedimento restrito,
que é justamente o algoritmo alvo com o qual se deseja comparar os algoritmos com 2-
níveis de checkpoints bidimensionais 1 e 2, propostos nesse trabalho.
Para uma melhor comparação visual dos dados obtidos no conjunto de testes do
experimento 1 deste trabalho com aqueles obtidos por Tarnas e Hughey (1998) em seus
experimentos, foram elaborados dois gráficos, veja Figuras 5.7 e 5.8, com os dados em
valores absolutos, referentes aos dados sintetizados na Tabela 5.5, além daqueles
normalizados (básico = 1) mostrados nas Figuras 5.3 e 5.4. Os resultados obtidos por
Tarnas e Hughey (1998) são apresentados nas Figuras 5.9 e 5.10 e foram extraídos da
Figura 3 do trabalho deles.
FIGURA 5.9 – Tempos de CPU para efetuar 4 execuções de computações de programação dinâmica de seqüências de aminoácidos contra um HMM com 500 nós. FONTE: Extraído da Figura 3 de Tarnas e Hughey (1998).
Os comportamentos obtidos no experimento 1 desta tese apresentam diferenças
significativas, com relação às medidas dos tempos de execução, daqueles obtidos por
174
Tarnas e Hughey (1998), na comparação com o algoritmo de Viterbi básico. Os
experimentos efetuados nesse estudo indicaram que o algoritmo de Viterbi com 2-níveis
de checkpoints por diagonais apresentaram queda de desempenho, em relação ao
algoritmo de Viterbi básico, por um fator de ~1,8 no intervalo anterior ao início de
atividade de paginação siginificativa, tanto para os tempos de CPU quanto para os
tempos totais de execução. Nesses experimentos, após o início de atividade significativa
de paginação pelo algoritmo de Viterbi básico, o algoritmo de Viterbi com 2-níveis de
checkpoints por diagonais apresentou um ganho de desempenho por um fator de ~1,4
para os tempos de CPU e de até ~5,0 para os tempos totais de execução. Ao passo que
os experimentos em Tarnas e Hughey (1998) apresentaram desempenhos bastante
próximos para os tempos de CPU em todo o intervalo considerado e para os tempos
totais de execução até o início de atividade de paginação siginificativa. Apresentaram
ganho de perfomance do algoritmo por diagonais sobre o básico, por um fator de até
~10 vezes para os tempos totais de execução, após o início de atividade de paginação
siginificativa. Veja a Tabela 5.5 e as Figuras 5.7, 5.8, 5.9 e 5.10.
FIGURA 5.10 – Tempos totais de execução para efetuar 4 execuções de computações de programação dinâmica de seqüências de aminoácidos contra um HMM com 500 nós. FONTE: Extraída da Figura 3 de Tarnas e Hughey (1998).
175
Os resultados do terceiro conjunto de testes, para alinhar seqüências de proteinas a um
HMM com 368 nós, estão resumidos na Tabela 5.7, eles são compostos pelos tempos de
CPU e pelos tempos totais de execução (Wall time). Para permitir um melhor
entendimento do comportamento destes resultados através da visualização gráfica,
foram confeccionados gráficos de linhas com base nos dados normalizados em relação
aos dados do algoritmo de Viterbi básico (básico = 1). Esses gráficos estampados nas
Figuras 5.11 e 5.12, referem-se aos tempos de CPU e tempos totais de execução,
respectivamente.
Pode ser verificado, observando-se a Tabela 5.7 e as Figuras 5.11 e 5.12, que o
algoritmo de Viterbi com 2-níveis de checkpoints por linhas com particionamento móvel
de memória e o algoritmo de Viterbi com 2-níveis de checkpoints por diagonais, com
particionamento móvel de memória e retrocedimento restrito, foram mais lentos do que
o algoritmo algoritmo de Viterbi básico. Eles foram em média 74,7% e 10,3% mais
lentos com relação aos tempos de CPU, respectivamente, e em média 72,4% e 10,1%
com relação aos totais de exucução, respectivamente.
TABELA 5.7 – Resultados dos Tempos de CPU e dos Tempos Totais de Execução (Wall Time) do Terceiro Experimento para os Algoritmos de Viterbi (Tempos em Segundos).
Viterbi com 2-níveis de checkpoints (por) Viterbi Básico linhas diagonais proposto 1 proposto 2 Comp.
seqüên. CPU Wall CPU Wall CPU Wall CPU Wall CPU Wall 100 0,011 0,012 0,018 0,019 0,011 0,012 0,010 0,011 0,010 0,011 200 0,022 0,024 0,036 0,038 0,022 0,024 0,020 0,021 0,020 0,020 300 0,033 0,035 0,055 0,057 0,036 0,038 0,030 0,032 0,030 0,031 400 0,044 0,046 0,075 0,077 0,050 0,050 0,040 0,041 0,040 0,040 500 0,055 0,058 0,096 0,098 0,062 0,064 0,050 0,052 0,050 0,051 600 0,066 0,068 0,119 0,120 0,075 0,077 0,060 0,061 0,060 0,060 700 0,081 0,082 0,142 0,144 0,090 0,091 0,070 0,072 0,070 0,071 800 0,087 0,089 0,161 0,164 0,100 0,103 0,079 0,080 0,075 0,079 900 0,100 0,104 0,189 0,192 0,115 0,120 0,091 0,092 0,090 0,091
1.000 0,115 0,117 0,206 0,210 0,130 0,131 0,099 0,099 0,095 0,099
Observando-se a Tabela 5.7 e as Figuras 5.11 e 5.12 pode-se notar a superioridade dos
algoritmos propostos 1 e 2 em relação aos demais algoritmos, tanto no que tange aos
176
tempos de CPU quanto aos tempos totais de execução. Os algoritmos propostos 1 e 2
foram superiores em desempenho até mesmo ao próprio algorimo de Viterbi básico: eles
foram em média 9,5% e 11%, respectivamente, mais rápidos do que o algorimo de
Viterbi básico para os tempos de CPU; e foram em média 10,7% e 12,4%,
respectivamente, mais rápidos do que o algorimo de Viterbi básico para os tempos de
totais de execução. Eles foram superiores em desempenho ao algoritmo de Viterbi com
2-níveis de checkpoints por diagonais, com particionamento móvel de memória e
retrocedimento restrito: o algoritmo por diagonais foi em média 22% e 24% mais lento
que os algoritmos propostos 1 e 2, respectivamente, com relação aos tempos de CPU; e
foi em média 23,6% e 25,7% mais lento que os algoritmos propostos 1 e 2,
respectivamente, com relação aos tempos totais de execução.
0,00
0,20
0,40
0,60
0,80
1,00
1,20
1,40
1,60
1,80
2,00
1 2 3 4 5 6 7 8 9 10
Tamanhos das seqüências (x 100 radicais)
Tem
pos d
e C
PU n
orm
aliz
ados
(bás
ico
= 1)
Viterbi Básico
2-níveis Checkpoints Linha
2-níveis Checkpoints Bidimensional 1
2-níveis Checkpoints Bidimensional 2
2-níveis Checkpoints Diagonais
FIGURA 5.11 – Tempos de CPU dos algoritmos Viterbi para alinhamento de seqüências de aminoácidos a um HMM com 368 nós.
É importante salientar que, também, para esse terceiro conjunto de experimentos o
algoritmo proposto 1 foi ligeiramente superior ao algoritmo proposto 2: ele foi em
177
média 1,6% mais rápido com relação aos tempos de CPU e em média 1,8% mais rápido
com relação totais de execução. Nota-se, novamente, que esse ganho em desempenho
talvez se deva ao fato que o algoritmo proposto 2 tire mais proveito do uso do
procedimento de retrocedimento restrito, mesmo para seqüências pequenas, com
comprimentos variando de 100 a 1000 símbolos.
0,00
0,20
0,40
0,60
0,80
1,00
1,20
1,40
1,60
1,80
2,00
1 2 3 4 5 6 7 8 9 10
Tamanhos das Seqüências (x 100 radicais)
Tem
pos t
otai
s de
exec
ução
nor
mal
izad
os (b
ásic
o =
1)
Viterbi Básico
2-níveis Checkpoints Linha
2-níveis Checkpoints Bidimensional 1
2-níveis Checkpoints Bidimensional 2
2-níveis Checkpoints Diagonais
FIGURA 5.12 – Tempos totais de execução dos algoritmos Viterbi para alinhamento de seqüências de aminoácidos a um HMM com 368 nós.
Analisando os resultados obtidos nos experimentos desta tese, nota-se que o algoritmo
de Viterbi com 2-níveis de checkpoints por linhas e particionamento móvel de memória
se comportou como o esperado para essa família de algoritmos com checkpoints.
Primeiro, o seu requerimento de memória, supondo m = n, que é da ordem de ( )nnO –
pois nnnnR 22 ≤≈ – foi sempre cerca de 3,2% do requerimento do algoritmo de
Viterbi básico. Segundo, ele apresentou perda de performance por um fator ~1,8 vezes o
algoritmo de Viterbi básico em condições de uso de memória virtual baixo e ganho de
performance por um fator de no máximo ~1,4 vezes para os tempos de CPU e de no
máximo ~5,0 vezes para os tempos totais de execução, após o algoritmo de Viterbi
178
básico começar a fazer uso significativo da memória virtual. Terceiro, ele itera de forma
regular sobre a matriz de PD, fazendo uso adequado da hierarquia de memória do PC.
Quarto, o comportamento do crescimento dos seus tempos de execução foi proporcional
ao comportamento de seus requerimentos de memória. Além disso, ele não fez uso do
procedimento de retrocedimento restrito.
Da análise dos resultados obtidos nos experimentos desta tese, nota-se que o algoritmo
de Viterbi com 2-níveis de checkpoints propostos 1 e 2 se comportaram melhor que o
esperado, de uma maneira geral, para essa família de algoritmos com checkpoints.
Primeiro, o seu requerimento de memória, supondo m = n, que é da ordem de ( )nnO –
pois nnR 4= para o proposto 1 e nnR 2= para o proposto 2 – foi sempre
decrescente para tamanhos crescentes de seqüências, em relação ao requerimento do
algoritmo de Viterbi básico, chegando, inclusive, a ser menor do que aqueles do
algoritmo de Viterbi por linhas para as seqüências maiores, que são as que interessam.
Segundo, ele apresentou, nos experimentos 1 e 2, ganho sobre todos os outros
algoritmos: ganho de no mínimo 5,2% sobre o algoritmo de Viterbi básico em condições
de uso de memória virtual baixo e ganho de performance por um fator de no mínimo ~2
vezes para os tempos de CPU e de ~8,0 a 10 vezes para os tempos totais de execução,
após o algoritmo de Viterbi básico começar a fazer uso significativo da memória virtual.
Terceiro ele apresentou desempenho superior aos demais algoritmos para o alinhamento
de seqüências pequenas a um HMM também pequeno, conforme os resultados do
experimento 3, com ganho de performance de aproximadamente 10% em relação ao
algoritmo de Viterbi básico e superior a 20% em relação ao algoritmo por diagonais.
Quarto, ele itera de forma regular sobre a matriz de PD, fazendo uso adequado da
hierarquia de memória do PC. Quinto, o comportamento do crescimento dos seus
tempos de execução foi proporcional ao comportamento de seus requerimentos de
memória. Além disso, ele fez uso do procedimento de retrocedimento restrito e se
beneficia do aumento de desempenho proporcionado por esse procedimento,
principalmente quando se usa menos linhas, R, de memória para as mesmas instâncias
do problema.
179
Com relação ao algoritmo de Viterbi com 2-níveis de checkpoints por diagonais, com
particionamento móvel de memória e retrocedimento restrito, analisando-se os
resultados obtidos nos experimentos, nota-se que ele se comportou aquém do esperado,
em geral, para essa família de algoritmos com checkpoints. Primeiro, o seu
requerimento de memória, supondo m = n, que é da ordem de ( )nnO – pois
nnnndnR 884 ≤=≈ – foi sempre crescente para tamanhos crescentes de
seqüências, em relação ao requerimento do algoritmo de Viterbi básico; com os
requerimentos de memória do algoritmo por diagonais tendendo ao infinito mais
rapidamente do que o algoritmo básico, quando o tamanho das seqüências tendem ao
infinito. Segundo, ele apresentou comportamento de desempenho inferior ao algoritmo
por linhas, pois além de poucas vezes superá-lo por um fator irrisório, não foi capaz de
computar alinhamentos de seqüências acima de 64.000 resíduos. Ele, também,
apresentou perda de performance por um fator ~1,8 vezes com relação ao algoritmo de
Viterbi básico em condições de uso de memória virtual baixo e ganho de performance
por um fator de no máximo ~1,4 vezes para os tempos de CPU e de no máximo ~5,0
vezes para os tempos totais de execução, após o algoritmo de Viterbi básico começar a
fazer uso significativo da memória virtual. Terceiro, mas dentro do critério de escolha
dos tamanhos de seqüências ele só conseguiu alinhar seqüências de até 64.000 resíduos,
já com uso intenso de memória virtual. Quarto, ele itera de forma regular sobre a matriz
de PD, fazendo uso adequado da hierarquia de memória do PC. Quinto, o
comportamento do crescimento dos seus tempos de execução foi proporcional ao
comportamento de seus requerimentos de memória, que crescem mais rapidamente do
que os requerimentos do algoritmo básico. Além disso, ele fez uso do procedimento de
retrocedimento restrito.
Sabe-se, ao menos teoricamente, que o algoritmo de Viterbi com 2-níveis de
checkpoints por diagonais, com particionamento móvel de memória e sem o
retrocedimento restrito, para as mesmas instâncias do problema, computa matrizes de
PD ~2 vezes maiores que os demais algoritmos considerados nos experimentos desta
tese, e que isto acarreta uma perda de performance de ~2 vezes em relação ao algoritmo
de Viterbi com 2-níveis de checkpoints por linhas, com particionamento móvel de
180
memória e sem retrocedimento restrito. Além disso foi levantado que existiria perda de
performance devido ao tratamento das condições de contorno para computar diagonais
ao invés de linhas ou colunas. Mas Tarnas e Hughey (1998) esperavam que, com a
adição do procedimento de retrocedimento restrito, o algoritmo por diagonais pudesse
superar o algorimo por linhas. Entretanto, eles realizaram e divulgaram experimentos
onde o algoritmo por diagonais apresentou um desempenho quase próximo ao
desempenho do algoritmo de Viterbi básico em condições normais de uso da memória
virtual e superior para condições de uso intenso da memória virtual.
Adicionalmente, nas suas conclusões eles relataram que o ganho de performance não foi
equivalente ao aumento de recursos de memória alocados ao algoritmo e às dificuldades
impostas pelas condições de contorno para computar diagonais ao invés de linhas ou
colunas, tanto no nível de codificação do algoritmo, quanto no nível de desempenho
deste, e planejaram retornar ao desenvolvimento do algoritmo por linhas.
Por outro lado, os resultados do presente estudo apontam, principalmente com relação
ao comportamento assintótico dos requerimentos de memória, mas também com relação
ao desempenho, que o algoritmo com 2-níveis de checkpoints por diagonais parece
bastante limitado para ser tomado como uma alternativa consistente ao algoritmo básico
para a computação de matrizes de PD, quando os tamanhos das instâncias do problema
implicam em exaustão dos recursos de memória do PC pelo algoritmo básico. O que é
agravado, tanto pelo comportamento assintótico de requerimentos de memória dos
outros algoritmos considerados, quanto pelo desempenho apresentado por esses nos
testes efetuados nesse estudo. O algoritmo por diagonais só pôde computar
alinhamentos de seqüências de comprimentos até 64.000 resíduos, enquanto os demais
algoritmos, com exceção do básico, computaram seqüências de tamanhos até 512.000
resíduos e apresentaram desempenho superior a esse.
Os algoritmos propostos 1 e 2 apresentaram o melhor comportamento assintótico dos
requerimentos de memória e tiveram desempenhos melhores que os demais algoritmos
considerados nos experimentos. Considerando instâncias grandes do problema de
alinhamento de seqüências a um HMM com 2.000 nós os algoritmos propostos foram
181
bem melhores que o segundo colocado, o algoritmo de Viterbi com 2-níveis de
checkpoints por linhas, com particionamento móvel de memória (sem retrocedimento
restrito). Eles apresentaram ganho de desempenho de ~2 vezes para instâncias do
problema que provoquem uso moderado da memória virtual, e ganhos maiores para
instâncias do problema que provoquem uso intenso da memória virtual, ganho de ~5
vezes nos tempos de CPU e de ~12 vezes nos tempos totais de execução. Mesmo
considerando os resultados do experimento 3 para instâncias pequenas do problema,
quando foram alinhadas seqüências de 100 a 1000 resíduos a um HMM com 368 nós, os
algoritmos propostos 1 e 2 foram cerca de 10% mais rápidos que o algorito básico e
cerca de 20% mais rápidos do que o algoritmo por diagonais. O que indica,
possivelmente, um melhor uso da hierarquia de memória do PC – principalmente da
memória cache – pelo algoritmos propostos 1 e 2 do que o algoritmo de Viterbi com 2-
níveis de checkpoints por linhas.
Essa maior eficiência no uso da hierarquia do sistema de memória do PC,
particularmente com relação ao uso da cache, se deve à forma de particionamento da
matriz de PD em seções pelos algoritmos propostos, em forma de grade. Desta forma na
fase de retrocedimento as seções a serem recomputadas pelos algoritmos propostos têm
linhas de comprimentos menores ou iguais a 2R
n (máximos), contra linhas de
tamanhos n (tamanho da seqüência) no caso do algoritmo por linhas. Isto ressalta a
eficiência no uso da hierarquia do sistema de memória do PC por esses algoritmos
propostos 1 e 2. É importante salientar que o uso de retrocedimento restrito pode reduzir
significativamente os tamanhos destas seções.
Considerando-se apenas 2-níveis de checkpoints, os algoritmos testados no presente
estudo não fazem uso de recursividade, nem durante a primeira fase de cálculos para
frente e nem durante a segunda fase de cálculos para trás, durante o retrocedimento.
Entretanto, se forem considerados níveis mais altos de checkpoints, os membros desta
família de algoritmos, que recorrem ao método de checkpoints e que adotam
particionamento da matriz de PD apenas em uma dimensão, precisam, durante a fase de
retrocedimento, recorrer a processos recursivos, ainda que não explicitamente. Já
182
aqueles que adotam particionamento da matriz de PD em duas dimensões não fazem uso
de nenhum processo recursivo, nem durante a fase para frente e, nem durante a fase de
retrocedimento.
Verifica-se, na prática, que o algoritmo de Viterbi com 2-níveis de checkpoints
bidimensionais por linhas e colunas, com particionamento fixo de memória e
retrocedimento restrito, utiliza de forma mais eficiente o sistema de memória,
particularmente com relação ao uso da cache, que os membros com 2-níveis de
checkpoints por linhas. Isto indica que o uso explícito e não recursivo do princípio D&C
em conjunto com o paradigma de checkpoints se beneficia de uso mais eficiente da
hierarquia de memória do PC, além de reduzir significativamente a quantidade de
recalculações nas seções da matriz de PD.
183
184
CAPÍTULO 6
CONCLUSÕES E COMENTÁRIOS
Os HMMs lineares e mais precisamente os perfis-HMMs que são os modelos que
motivaram essa tese, utilizam métodos de PD para o cálculo de medidas de desempenho
e para o treinamento (cálculo dos parâmetros) do modelo. Os algoritmos que
implementam esses métodos de PD, que são o objeto deste estudo, podem se beneficiar
de técnicas ou métodos de economia de espaço (memória de trabalho). Essa economia
de espaço, geralmente, é obtida em troca de uma queda de desempenho no tempo de
execução do algoritmo.
Existem na literatura pertinente duas classes principais de algoritmos de PD usados na
comparação de strings ou seqüências de símbolos, que recorrem a técnicas (métodos ou
paradigmas) de economia de espaço. Uma delas fundamentada no princípio D&C e a
outra no paradigma de checkpoints. O algoritmo pioneiro de Hirschberg (1975) é o
principal algoritmo da classe de algoritmos que adotam primariamente o princípio
D&C para economia de espaço, sendo os demais variantes deste, ou que agregam
secundariamente outras técnicas ao núcleo deste (programação dinâmica, caminhos
mínimos, contornos, vetores de bit), ou que estendem o original para efetuar outros tipos
de comparações. Esses algoritmos têm sido utilizados na computação da menor
substring comum (LCS) entre duas strings (ou do problema dual do script de edição
mínimo) e na comparação de pares de strings ou seqüências, usando funções de custo
um pouco mais complexas, como funções de custo afim que foram apresentadas por
Myers e Miller (1988), entretanto, não existe, até onde se conhece, na literatura
nenhuma aplicação destes algoritmos D&C em conjunto com HMMs.
Talvez, alguns dos algoritmos desta classe, fundamentada no princípio D&C, possam
ser estendidos para comparar seqüências a um perfil-HMM e recuperar o melhor
caminho de estados, que provavelmente tenha produzido determinada seqüência de
observações O. Contudo, esses métodos não são convenientes para serem usados com
procedimentos de retrocedimento completo sobre a matriz de PD, quando é necessário
recuperar todos os caminhos de estados num HMM, que provavelmente tenham
185
produzido uma dada seqüência de observações O. Mas, ainda permanecem como
desafio, a utilização das idéias usadas nesses algoritmos no desenvolvimento de outros
algoritmos que possam ser usados para computar medidas de interesse nos perfis-
HMMs.
A outra classe se refere a uma família de métodos, apresentada no Capítulo 3, que
fazem uso do método de PD e do paradigma (ou estratégia) de checkpoints para
comparação e/ou alinhamento de seqüências com economia de espaço – com
necessidade reduzida de uso de memória de trabalho – que podem ser usados com
procedimentos de retrocedimento tanto parciais quanto completos, e que são
convenientes para implementação tanto serial quanto paralela. Numa implementação
serial eles requerem uso de memória da ordem de O( L nm ) com um fator de queda de
desempenho proporcional a kL, com 0 1≤< k . Desta forma, essa família de métodos
com o paradigma de checkpoints para economia de espaço se constitui na opção mais
interessante para serem usadas, quando se requer tanto a aplicação de retrocedimento
parcial, quanto a de retrocedimento completo sobre a matriz de PD, para a computação
de medidas de interesse.
Quando se usa perfis-HMMs para análise e modelagem de seqüências biológicas é
necessário se obter medidas ou cálculos que requeiram tanto os procedimentos de
retrocedimento parcial, quanto os de retrocedimento completo. Desta forma, o presente
estudo voltou-se exclusivamente para essa segunda classe de algoritmos de PD, que
fazem uso do paradigma de checkpoints como método de economia de espaço.
No presente estudo foi estudado um conjunto de algoritmos desta família de algoritmos
de PD com L-níveis de checkpoints, dos quais não foram encontradas qualquer
referência na literatura pertinente, que usam o paradigma de checkpoints em conjunto
com o princípio D&C, recorrendo a armazenagem de checkpoints nas duas dimensões
da matriz de PD – linhas e colunas. Esses algoritmos foram denominados algoritmos
com L-níveis de checkpoints bidimensionais e foram detalhados no Capítulo 4. Os
estudos da complexidade destes algoritmos foram restritos àqueles com particionamento
186
fixo de memória, entretanto eles podem ser estendidos, sem perda de generalidade, para
cobrir o caso de particionamento móvel de memória.
Para efeito de análise, comparação e experimentação, o presente estudo se concentrou
nos algoritmos com 2-níveis de checkpoints com retrocedimento parcial usados na
comparação e alinhamento de seqüências a um perfil-HMM. Um destes algoritmos
propostos, o de Viterbi com 2-níveis de checkpoints bidimensionais com
particionamento fixo de memória e retrocedimento restrito, foi escolhido para
realização, a posteriori, de testes de desempenho numa comparação com o algoritmo de
Viterbi com 2-níveis de checkpoints por diagonais, com particionamento móvel de
memória e retrocedimento restrito – considerado o estado da arte desta família de
algoritmos com L-níveis de checkpoints – e com o algoritmo de Viterbi com 2-níveis de
checkpoints por linhas com particionamento móvel de memória.
Os estudos teóricos indicaram, para instâncias genéricas do problema, e sem considerar
a aplicação de procedimento de retrocedimento restrito, que esses algoritmos de Viterbi
com L-níveis checkpoints bidimensionais e particionamento fixo de memória
apresentam um comportamento assintótico de requerimentos de espaço e de tempos de
execução semelhantes aos demais membros desta família. Considerando-se m = n e um
nível L arbitrário, os requerimentos de tempos são da ordem de ( )2nO e os
requerimentos de espaço são da ordem de ( )L nnO para os algoritmos bidimensionais e
para os algoritmos unidimensionais por linhas, ou por colunas, ou por diagonais.
Entretanto, se forem considerados os coeficientes das funções de complexidade
assintótica, verifica-se uma superioridade em desempenho dos algoritmos de Viterbi
com L-níveis de checkpoints bidimensionais 1 e 2. Os requerimentos de memória do
algoritmo de Viterbi com L-níveis de checkpoints por linhas ou colunas são dados por LL nLnLR ≤≤ ! , apresentando portanto um coeficiente kL, 10 << k , para a função de
complexidade de espaço. O algoritmo de Viterbi com L-níveis de checkpoints por
diagonais tem requerimento de espaço dado por LLn 42! ≤L nLR 2≤ , apresentando
portanto um coeficiente 4kL, 10 << k . Enquanto o algoritmo de Viterbi com L-níveis
187
de checkpoints bidimensionais 1 tem requerimento de espaço dado por LL nnR 22 ≤≤ ,
apresentando portanto um coeficiente 2, o algoritmo de Viterbi com L-níveis de
checkpoints bidimensionais 2 tem requerimento de espaço dado por L nR ≤ ,
apresentando portanto um coeficiente 1. Essa redução nos requerimentos de espaço é
obtida em troca de uma queda de desempenho. Os algoritmos de Viterbi com L-níveis
de checkpoints bidimensionais 1 e 2 apresentam queda de desempenho dada por um
coeficiente proporcional a 2k, com 0 1≤< k
10
. Já o algoritmo de Viterbi com L-níveis de
checkpoints por linhas ou colunas apresenta queda de desempenho dada por um
coeficiente proporcional a kL, com ≤< k , e o algoritmo de Viterbi com L-níveis de
checkpoints por diagonais apresenta queda de desempenho dada por um coeficiente
proporcional a 2kL, com (detalhes nos Capítulos 3 e 4). 10 ≤< k
( )3nO
Adicionalmente, pode-se verificar que a superioridade dos algoritmos de Viterbi com L-
níveis de checkpoints bidimensionais 1 e 2 é maior quando é levado em consideração
uma análise de pior caso. Numa análise de pior caso pode-se tomar um nível L = n de
checkpoints. Para essas instâncias do problema o algoritmo de Viterbi com L-níveis de
checkpoints por linhas (ou colunas) e o de Viterbi com L-níveis de checkpoints por
diagonais com particionamento móvel de memória apresentam complexidade assintótica
de tempo da ordem de , com coeficiente de complexidade igual a 1 para o
algoritmo por linhas (ou colunas) e igual 2 para o algoritmo por diagonais. Enquanto a
complexidade dos algoritmos de Viterbi com L-níveis de checkpoints bidimensionais 1 e
2 com particionamento fixo de memória é uma ordem de magnitude mais rápido,
apresentando complexidade assintótica da ordem de ( )2nO , com coeficiente de
complexidade igual a 2. Para essas instâncias do problema os requerimentos de
memória destes algoritmos são mínimos, com o algoritmo por linhas e o algoritmo por
colunas requerendo apenas duas linhas de memória, R = 2 checkpoints, o algoritmo por
diagonais requerendo três linhas de memória, R = 3, e os algoritmos bidimensionais
requerendo 4 linhas, R = 4.
Quando a análise aborda instâncias onde o nível de checkpoints é tomado como sendo
, o comportamento destes algoritmos de Viterbi com L-níveis de checkpoints nL blog=
188
são menos discrepantes em termos dos coeficientes de complexidade. Os algoritmos
bidimensionais de Viterbi com L-níveis de checkpoints apresentam complexidade da
ordem de ( )2nO , com queda de desempenho dada por um fator de no máximo 1 vez,
enquanto o algoritmo de Viterbi com L-níveis de checkpoints por linhas, ou por colunas,
ou por diagonais apresenta ordem de complexidade de ( )2nO
~ e
, com queda de
desempenho dada por um fator de ~ vezes,
respectivamente. Para essas instâncias do problema, o comportamento assintótico da
complexidade dos requerimentos de memória são praticamente uma ordem de
magnitude menores para todos esses algoritmos, eles são da ordem de O .
nblog~ ,
L blog
nblog nblog2
nn blog
L =
( )
=
Mesmo quando é adicionado um procedimento de retrocedimento restrito ao algoritmo
de Viterbi com L-níveis de checkpoints por diagonais e particionamento móvel de
memória, o comportamento assintótico da complexidade de tempo deste ainda continua
sendo inferior ao dos algoritmos de Viterbi com L-níveis de checkpoints bidimensionais
e particionamento fixo de memória, mesmo sem a utilização do procedimento de
retrocedimento restrito pelos algoritmos bidimensionais, além de requerer 2L ou 4L
vezes mais memória do que o algroritmos bidimensionais para computar uma
determinada matriz de PD para um dado nível L de checkpoints.
Grice et al. (1997, p. 51) concluíram que não existe qualquer penalidade na performance
do algoritmo de Viterbi com L-níveis de checkpoints por diagonais, com
particionamento móvel de memória e retrocedimento restrito, quando o nível
. Mas, seguindo os passos das demonstrações esboçadas por eles no relatório
pode-se mostrar que, ao contrário da conclusão apresentada por eles, o algoritmo por
diagonais sofre uma perda de performance de até 2 vezes, quando o nível ,
veja mais detalhes na Seção 3.3.5, página 94. Enquanto, no presente estudo foi mostrado
que o algoritmo proposto, para um nível
nL blog=
n2log
n , o algoritmo de Viterbi com L-
níveis de checkpoints bidimensionais e particionamento fixo de memória não apresenta
perda de performance, mesmo sem a necessidade de recorrer a um procedimento de
retrocedimento restrito, veja mais detalhes na Seção 4.2.4, página 118.
189
Do exposto nos parágrafos anteriores, foi verificado que os coeficientes da
complexidade de tempo e de espaço do algoritmo de Viterbi com L-níveis de
checkpoints por diagonais e particionamento móvel de memória, mesmo quando esse
recorreu ao uso de retrocedimento restrito, se mostraram, sempre, superiores aos
coeficientes dos outros algoritmos com checkpoints considerados nesse estudo. Desta
forma, do ponto de vista teórico, esse algoritmo por diagonais se constitui na escolha
mais pobre como alternativa ao algoritmo de Viterbi básico, para instâncias intratáveis
por esse, numa determinada máquina (PC) serial, do que os outros algoritmos com
economia de espaço: o algoritmo de Viterbi com L-níveis de checkpoints por linhas (ou
colunas) e particionamento móvel de memória e os algoritmos de Viterbi com L-níveis
de checkpoints bidimensionais e particionamento fixo de memória.
Foi verificado, assim, que o algoritmos propostos, algoritmos de Viterbi com L-níveis
de checkpoints bidimensionais e particionamento fixo de memória, foram os que
apresentaram os menores coeficientes para as complexidades de espaço e de tempo
dentre esses algoritmos com economia de espaço, considerados no presente estudo.
Portanto, do ponto de vista teórico, esses algoritmos propostos se constituem na escolha
mais consistente, dentre os algoritmos considerados, como alternativa ao algoritmo de
Viterbi básico, para instâncias intratáveis por esse algoritmo básico numa determinada
máquina (PC) serial. Se a esses algoritmos bidimensionais forem agregados um
procedimento de retrocedimento restrito, essa superioridade ficará ainda mais evidente.
Entretanto, é bom salientar que o algoritmo de Viterbi com L-níveis de checkpoints por
linhas (ou colunas) e particionamento móvel de memória obteve coeficientes bem
melhores do que o algoritmo por diagonais e portanto, também, se constitui em uma
escolha boa como alternativa ao algoritmo de Viterbi básico.
A utilização de técnicas ou métodos de economia de espação, a princípio, é útil para o
tratamento de instâncias do problema que não podem ser manipuladas por um algoritmo
padrão (ou básico). Desta forma o comportamento assintótico dos requerimentos de
espaço e a penalização que se paga em troca, em termos de tempos de execução, devem
ser levados em consideração. Supondo que o algoritmo básico requeira uma quantidade
190
de memória definida como MEM, espera-se que os requerimentos dos algoritmos com
economia de espaço sejam uma fração dos requerimentos de espaço do algoritmo
básico, ou seja k×MEM para 10 ≤< k , principalmente para instâncias do problema que
não podem ser manipulados pelo algoritmo básico. Melhor ainda se fossem limitados
por alguma função de complexidade menor do que aquela da complexidade do
requerimento de espaço do algoritmo básico, ou se fossem constantes.
Foi verificando o comportamento assintótico dos requerimentos de memória destes
algoritmos com 2-níveis de checkpoints em relação aos requerimentos de memória do
algoritmo de Viterbi básico, através de uma simulação das estimativas da memória
necessária para acomodar as principais estruturas de dados.
Essa simulação do comportamento assintótico dos requerimentos de memória do
algoritmo de Viterbi com 2-níveis de checkpoints por diagonais, com particionamento
móvel de memória e retrocedimento restrito apresentou indícios de que os requisitos de
memória deste crescem para o infinito mais rapidamente que os requisitos de memória
do algoritmo de Viterbi básico, quando o tamanho das seqüências crescem
indefinidamente. Sendo esses requerimentos maiores do que os do algoritmo de Viterbi
básico já para seqüências com 2.048.000 resíduos, o que indica um comportamento
assintótico de requerimentos de espaço contrário ao esperado, pelo menos para os
algoritmos com 2-níveis de checkpoints. Desta forma, esse algoritmo com 2-níveis de
checkpoints por diagonais parece não constituir uma alternativa consistente ao algoritmo
de Viterbi básico.
O comportamento assintótico dos requerimentos de espaço do algoritmo de Viterbi com
2-níveis de checkpoints por linhas e particionamento móvel de memória está em
conformidade com o esperado. Esse requerimento foi sempre uma fração constante de
~3,2% relativamente ao algoritmo de Viterbi básico. Sendo, desta forma uma alternativa
consistente ao algoritmo de Viterbi básico.
O comportamento assintótico dos requerimentos de espaço dos algoritmos de Viterbi
com 2-níveis de checkpoints bidimensionais e particionamento fixo de memória está em
conformidade com o esperado. Esse requerimento foi sempre uma fração decrescente
191
relativamente ao algoritmo de Viterbi básico. Constituindo-se, desta forma na melhor
alternativa consistente ao algoritmo de Viterbi básico.
Foram realizados no presente estudo um conjunto de testes divididos em três
experimentos para análise, a posteriori, do desempenho dos algoritmos de Viterbi no
alinhamento de seqüências a um perfil-HMM. Os algoritmos bidimensionais,
denominados algoritmos propostos 1 e 2, foram considerados com duas configuraçães
de requerimentos de memória: o algoritmo proposto 1 supostamente usaria uma
quantidade de memória próxima àquela requerida pelo algoritmo por diagonais, mas na
prática, conforme o exposto em parágrafos anteriores, ela foi assintóticamente muito
menor; e o algoritmo proposto 2 que usou metade da quantidade de memória do
algoritmo proposto 1.
Os testes relativos aos experimentos 1 e 2 mostraram que o algoritmo de Viterbi básico
provocou uso intenso de memória virtual para o alinhamento de seqüências com 32.000
resíduos, gastando mais tempo na troca de páginas entre memória principal e memória
virtual do que no processamento do alinhamento, e não conseguiu alocar memória
suficiente para alinhar seqüências com 64.000 resíduos. O mesmo aconteceu com o
algoritmo com 2-níveis de checkpoints por diagonais, ele provocou atividade intensa de
paginação para o alinhamento de seqüências com 64.000 e não conseguir alinhar
seqüências com 128.000 resíduos. Outro algoritmo que provocou atividade intensa de
paginação foi o algoritmo com 2-níveis de checkpoints por linhas, ele provocou o uso
intenso de memória virtual pelo PC para seqüências com 512.000 resíduos. Esses dados
indicam uma vantagem para os algoritmos propostos 1 e 2, que não iniciaram atividade
significativa de paginação para nenhum tamanho de seqüência no intervalo considerado,
2.000 a 512.000 resíduos, podendo portanto alinhar seqüências bem maiores do que os
demais algoritmos considerados.
Esses testes relativos aos 3 experimentos mostraram que os algoritmos propostos 1 e 2
obtiveram desempenho bastante próximos um do outro, com ligeira vantagem para o
algoritmo proposto 2. Mostraram ainda que eles obtiveram um desempenho melhor do
que o próprio algoritmo de Viterbi básico, para todas as instâncias do problema, que
192
foram consideradas, foram pelo menos 5,2% mais rápido em condições de uso de
memória virtual baixo que algoritmo de Viterbi básico. Dos experimentos 1 e 2 pode-se
verificar que eles obtiveram ganho de performance por um fator de pelo menos 2 vezes
para os tempos de CPU e de pelo menos 8,0 a 10 vezes para os tempos totais de
execução, após o algoritmo de Viterbi básico começar a fazer uso significativo da
memória virtual. Dos testes do experimento 3 pode-se verificar que os algoritmos
propostos obtiveram desempenho superior ao algoritmo de Viterbi básico, em média,
cerca de 10% e 11% com relação aos tempos de CPU e aos tempos totais de execução,
respectivamente. Esses dados indicam que os algoritmos propostos podem ser usados,
preferencialmente, no lugar do próprio algoritmo básico, tanto no alinhamento de de
seqüências pequenas, por exemplo proteínas, quanto no alinhamento de seqüências
grandes, por exemplo moléculas de DNA, cromossomos, genomas.
Nenhum dos outros dois algoritmos com 2-níveis de checkpoints conseguiu igualar essa
performance dos algorimos propostos para nenhuma das instâncias do problema que
foram consideradas nos conjuntos de testes. Com relação ao algoritmo de Viterbi com 2-
níveis de checkpoints por linhas, com particionamento móvel de memória, os algoritmos
propostos foram pelo menos 2 vezes mais rápidos, nos testes dos experimentos 1 e 2,
para instâncias do problema com uso moderado da memória virtual, e foram ainda mais
rápidos para instâncias do problema com uso intenso da memória virtual, com ganhos
de performance de até ~5 vezes para os tempos de CPU e de até ~12 vezes para os
tempos totais de execução. Já, com relação ao algoritmo de Viterbi com 2-níveis de
checkpoints por diagonais, com particionamento móvel de memória e retrocedimento
restrito, os algoritmos propostos foram pelo menos 2 vezes mais rápidos para instâncias
do problema com uso moderado da memória virtual, e foram ainda mais rápidos para
instâncias do problema com uso intenso da memória virtual, com ganhos de
performance de ~2 vezes para os tempos de CPU e de ~13 vezes para os tempos totais
de execução. Os dados dos testes do experimento 3 indicam que os algoritmos propostos
foram superiores em média cerca de 94% ao algoritmo de Viterbi com 2-níveis de
checkpoints por linhas e particionamento móvel de memória, e em média cerca de 24%
193
ao algoritmo de Viterbi com 2-níveis de checkpoints por diagonais, com
particionamento móvel de memória e procedimento de retrocedimento restrito.
Pode-se observar, também, dos resultados do experimento 3 que o algoritmo de Viterbi
básico foi superior em média cerca de 10% ao algoritmo de Viterbi com 2-níveis de
checkpoints por diagonais, com particionamento móvel de memória e procedimento de
retrocedimento restrito. Já esses resultados, do experimento 3, indicam que o algoritmo
de Viterbi com 2-níveis de checkpoints por diagonais, com particionamento móvel de
memória e procedimento de retrocedimento restrito, obteve desempenho superior em
média 36% ao algoritmo de Viterbi com 2-níveis de checkpoints por linhas e
particionamento móvel de memória.
Uma das características destes algoritmos de PD que utilizam o paradigma de
checkpoints é iterar de forma regular sobre a matriz de PD, o que possibilita um uso
mais adequado da hierarquia de memória do PC. Os resultados confirmaram esse
comportamento, indicando, possivelmente, um melhor uso da hierarquia de memória do
PC – principalmente da memória cache – pelos algoritmos de Viterbi com 2-níveis de
checkpoints por linhas, ou por diagonais, ou bidimensionais. Entretanto, os resultados
indicam um aproveitamento melhor desta característica pelos algoritmos propostos 1 e
2, do que pelos outros dois algoritmos, o que, possivelmente, se deve à forma de
particionamento da matriz de PD em seções pelos algoritmos propostos, em forma de
grade, possibilitando que os algoritmos propostos computem na fase de retrocedimento
“linhas” de comprimentos menores ou iguais a 2R
n (máximos), contra “linhas” de
tamanhos n (tamanho da seqüência) no caso dos outros dois algoritmos. É importante,
salientar que o uso de retrocedimento restrito pode reduzir significativamente os
tamanhos destas seções, e, portanto, pode reduzir o tamanho das “linhas” a serem
recomputadas.
Desta forma, pode-se verificar que o uso explícito e não recursivo do princípio D&C em
conjunto com o paradigma de checkpoints e com o procedimento de retrocedimento
restrito, pelos algoritmos propostos 1 e 2, reduziu significativamente a quantidade de
194
recalculos nas seções da matriz de PD, durante o processo de retrocedimento parcial.
Além do que a iteração regular sobre a matriz de PD e essa redução significativa nos
comprimentos das seções das “linhas” a serem computadas numa determinada iteração,
durante o procedimento de retrocedimento parcial, permitiram uma utilização mais
eficiente da hierarquia de memória do PC pelos algoritmos propostos 1 e 2.
O melhor desempenho do algoritmo proposto 2 sobre o algoritmo proposto 1 só pode
ser explicado pelo comportamento, já relatado, do uso do procedimento de
retrocedimento restrito nesses algoritmos com L-níveis de chekpoints. O algoritmo
proposto 2 se beneficiou mais do aumento de desempenho proporcionado por esse
procedimento de retrocedimento restrito, pois ele necessita apenas de metade das linhas,
R, de memória requeridas pelo algoritmo proposto 1, para as mesmas instâncias do
problema.
Sabe-se, ao menos teoricamente, que o algoritmo de Viterbi com 2-níveis de
checkpoints por diagonais, com particionamento móvel de memória e sem o
retrocedimento restrito, computa matrizes de PD ~2 vezes maiores, para as mesmas
instâncias do problema, que os demais algoritmos considerados nos experimentos desta
tese, e que isto acarreta uma perda de performance de ~2 vezes quando tomada em
relação ao algoritmo de Viterbi com 2-níveis de checkpoints por linhas, com
particionamento móvel de memória e sem retrocedimento restrito, e de ~4 vezes quando
tomada em relação ao algoritmo de Viterbi básico. Além disso, foi levantado a
existência de perda de desempenho devido ao tratamento das condições de contorno
para computar diagonais ao invés de linhas ou colunas.
Apesar disso, Tarnas e Hughey (1998) esperavam que, com a adição do procedimento
de retrocedimento restrito, o algoritmo de Viterbi com 2-níveis de checkpoints por
diagonais e particionamento móvel de memória pudesse superar o algorimo de Viterbi
com 2-níveis de checkpoints por linhas e particionamento móvel de memória. Contudo,
a análise apresentada na Seção 3.3.5 indica uma perda de performance de até 2 vezes
em relação ao algoritmo de Viterbi básico, quando foi acrescido um procedimento de
195
retrocedimento restrito ao algoritmo de Viterbi com 2-níveis de checkpoints por
diagonais.
Eles realizaram e divulgaram experimentos onde o algoritmo de Viterbi com 2-níveis de
checkpoints por diagonais, com particionamento móvel de memória e retrocedimento
restrito, apresentou um desempenho superior ao algoritmo de Viterbi com 2-níveis de
checkpoints por linhas e particionamento móvel de memória. O desempenho deste foi
muito próximo ao desempenho do algoritmo de Viterbi básico em condições normais de
uso da memória virtual, e foi superior para condições de uso significativo da memória
virtual. Entretanto, nas suas conclusões eles relataram que o ganho de performance não
foi equivalente ao aumento de recursos de memória alocados ao algoritmo e às
dificuldades impostas pelas condições de contorno para computar diagonais ao invés de
linhas ou colunas, ao nível de codificação e de desempenho do algoritmo. E planejaram
retornar ao desenvolvimento do algoritmo por linhas.
Os resultados dos experimentos realizados nessa tese estão mais consonantes com os
resultados esperados para o algoritmo de Viterbi com 2-níveis de checkpoints por
diagonais, com particionamento móvel de memória e retrocedimento restrito. A perda
de performance devido à computação de matrizes de PD cerca de 2 vezes maiores e às
complexidades adicionais advindas da computação por diagonais foi recuperada com a
aplicação do procedimento de retrocedimento restrito, possibilitando ao algoritmo por
diagonais com a adição de procedimento restrito empatar em desempenho, nos testes
dos experimentos 1 e 2, com o algoritmo de Viterbi com 2-níveis de checkpoints por
linhas e com particionamento móvel de memória, em condições normais de utilização
da memória virtual, conforme o esperado pela análise feita na Seção 3.3.5.
Por outro lado, os resultados desta tese apontam, principalmente com relação ao
comportamento assintótico dos requerimentos de memória, mas também com relação ao
desempenho, que o algoritmo com 2-níveis de checkpoints por diagonais, com
particionamento móvel de memória e retrocedimento restrito, parece bastante limitado
para ser tomado como uma alternativa consistente ao algoritmo básico para a
computação de matrizes de PD, quando os tamanhos das instâncias do problema
196
implicam em exaustão dos recursos de memória do PC pelo algoritmo básico. O
algoritmo por diagonais conseguiu alinhar somente seqüências de até 64.000 resíduos a
um HMM com 2.000 nós. Na simulação do crescimento dos requerimentos de memória
necessários para alinhar seqüências de tamanhos crescentes, o algoritmo por diagonais
apresentou taxa de crescimento maior do que a do algoritmo de Viterbi básico. Tudo
isso é agravado, tanto pelo comportamento assintótico de requerimentos de memória
dos outros algoritmos considerados no presente estudo, quanto pelo desempenho
apresentado por esses nos testes efetuados; eles computaram seqüências de tamanhos
até 512.000 resíduos e apresentaram desempenho igual, no caso do algoritmo por linhas,
ou 2 vezes superior, no caso dos algoritmos propostos, ao do algoritmo por diagonais no
intervalo de 2.000 a 32.000 resíduos.
Do exposto nos parágrafos anteriores deste capítulo pode-se concluir que os algoritmos
de Viterbi com 2-níveis de checkpoints bidimensionais, com particionamento fixo de
memória e retrocedimento restrito, são a melhor alternativa, a alternativa mais consiste,
ao algoritmo de Viterbi básico, para a computação de instâncias intratáveis por esse
algoritmo básico numa determinada máquina (PC) serial. Até mesmo para instâncias
tratáveis pelo algoritmo de Viterbi básico os algoritmos de Viterbi com 2-níveis de
checkpoints bidimensionais foram superiores em desempenho. Entretanto, é bom,
salientar que o algoritmo de Viterbi com L-níveis de checkpoints por linhas (ou colunas)
e particionamento móvel de memória, também, constitui uma escolha boa como
alternativa ao algoritmo de Viterbi básico.
Foi verificado, na análise teórica e nos testes de desempenho a posteriori realizados com
esses algoritmos de Viterbi com 2-níveis de checkpoints, que os algoritmos de Viterbi
com L-níveis de checkpoints bidimensionais, com particionamento fixo de memória e
retrocedimento restrito, são a melhor alternativa, a mais consiste, ao algoritmo de
Viterbi básico, para todas as instâncias, principalmente para aquelas intratáveis por esse
algoritmo básico numa determinada máquina (PC) serial. Pois, esses algoritmos
propostos apresentaram coeficientes de complexidade menores na análise teórica
quando comparado aos demais algoritmos, tiveram melhor comportamento assintótico
dos requerimentos de espaço que os demais algoritmos, relativamente ao algoritmo de
197
Viterbi básico, e foram sempre superiores aos demais nos requerimentos de tempo de
execução, inclusive ao próprio algoritmo de Viterbi básico. Desta forma, os algoritmos
propostos são convenientes tanto para efetuar buscas por seqüências similares de
proteínas num BD de seqüências, por exemplo no BD Swiss-Prot, que envolvem
comparações de todas as seqüências do BD ao HMM, efetuando, portanto, milhares de
comparações, quanto para efetuar comparações de seqüências grandes a um HMM, por
exemplo na comparação de genomas inteiros, que constituem instâncias intratáveis pelo
algoritmo de Viterbi básico.
Adicionalmente ao melhor desempenho, nas análises a priori e a posteriori, tanto ao
nível de requerimentos de espaço, quanto ao nível de requerimentos de tempo de
execução, esses algoritmos de Viterbi com L-níveis de checkpoints bidimensionais e
particionamento fixo de memória apresentam outras características, que os tornam ainda
mais atraentes. Esses algoritmos propostos, diferentemente, dos outros membros desta
família de algoritmos que fazem uso do paradigma de checkpoints são bastante flexíveis
com relação à quantidade de memória alocada para a computação de uma determinada
matriz de PD. Isto se deve principalmente a dois fatos ou particularidades, a saber:
• O particionamento fixo da memória disponível ou necessária para as
computações (ou re-computações), entre memória de trabalho e memória de
checkpoints;
• O uso explícito e não recursivo do princípio D&C, que requer uma quantidade
menor de memória de trabalho para recomputações de seções da matriz de PD
na fase de retrocedimento.
Desta forma, fixado a quantidade de linhas e colunas de checkpoints que serão usadas,
pode-se calcular a quantidade de linhas de memória de trabalho em função dos
requerimentos de memória de trabalho na fase de retrocedimento e do nível L de
checkpoints que será adotado, sendo que são necessárias no mínimo 2 linhas de
memória de trabalho na fase de computações para frente (forward), o que constitui um
requerimento mínimo. Ao contrário, para os outros algoritmos membros, fixado uma
198
quantidade de memória disponível ou necessária, pode-se escolher somente o nível de
checkpoints que pode acomodar está memória para a solução da matriz de PD.
Entretanto algumas questões ainda permanecem abertas. Elas podem ser objetos de
novos trabalhos, que poderiam estender o alcance dos resultados aqui auferidos. Podem
ser destacados as seguintes questões:
• Realização de estudos comparativos do comportamento assintótico dos
requerimentos de memória destes algoritmos com L-níveis de chekpoints
bidimensionais para outros valores de L, inclusive considerando . nL blog=
• Realização de estudos do desempenho a posteriori dos requerimentos de tempo
do algoritmo com L-níveis de chekpoints bidimensionais para outros valores de
L, inclusive considerando nL blog= .
• Verificar, na prática, o limite do pior caso de desempenho destes algoritmos, que
é de perda de desempenho por um fator de no máximo 2 vezes, com relação ao
requerimentos de tempo, para um nível L = n.
• Realização de estudos do desempenho a posteriori dos algoritmos de L-níveis de
checkpoints bidimensionais com partição fixa de memória em aplicações dos
HMMs que requeiram a utilização de procedimentos de retrocedimento
completo sobre a matriz de PD.
• Realização de estudos do desempenho a posteriori do algoritmo com 2-níveis de
chekpoints por linhas quando a esse é adicionado um procedimento de
retrocedimento restrito.
E ao nível mais geral, a realização de estudos da possibilidade de utilização das idéias
usadas para aceleração do desempenho dos algoritmos D&C, nos algoritmos de PD com
checkpoints ou no desenvolvimento de outros algoritmos, que possam ser usados para
computar medidas de interesse nos perfis-HMMs.
199
200
REFERÊNCIAS BIBLIOGRÁFICAS
Barret, C.; Hughey, R.; Karplus, K. Scoring hidden Markov models. CABIOS, v. 13,
n.2, 191-199, 1997.
Birney, E. Sequence Alignment in Bioinformatics. Tese (Doutorado em
Bioinformática) - The Sanger Centre in Cambridge, Hinxton, 133 p., 2000.
Birney, E. Hidden Markov Models in biological sequence analysis. IMB J. Res. &
Dev., v. 45, n. 3/4, p. 449-454, 2001.
Cappé, O. Ten years of HMMs. Paris, França. 16p., Mar. 2001. Disponível em:
http://www.tsi.enst.fr/~cappe/docs/hmmbib.html . Acesso em: Jun. 2001.
Comet, J. P. Programmation Dynamique et Alignements de Séquences Biologiques.
Tese (Doutorado em Controle de Sistemas) – Université de Technologie de
Compiègne, Compiègne, 214 p.,1998.
Couvreur, C.; Fontaine, V.; Gaunard, P.; Mubikangiey, C. G. Automatic Classification
of Enviromental Noise Events by Hidden Markov Models. Applied Acoustics, v.
54, n. 3, p. 187-206, 1998.
Crochemore, M.; Iliopoulos, C. S.; Pizon, Y. J. Speeding-up Hirschberg and Hunt-
Szymanski LCS algorithms. Fundamenta Informaticae, v. 56, n. 1 e 2, p. 89-103,
2003.
Cuesta, D.; Micó, P.; Aboy, M.; Novák_, D.; Brezny_, R.; Samblas_, L.; Pastor, D.;
Sancho, S. Biosignal Laboratory: A Software Tool for Complete Biomedical Signal
Processing and Analysis. In: International Conference of IEEE Engineering in
Medicine and Biology Society , 25. (EMBS), 2003, Cancun. Proceedings. IEEE, v.
4, p. 3544-3547, 2003.
Durbin, R.; Eddy, S.; Krogh, A.; Mitchison, G. Biological sequence analysis:
probabilistic models of proteins and nucleic acids. New York: Cambridge
University Press, 1998. 356p.
201
Eddy, S. R. Profile Hidden Markov Models. Bioinformatics, v.14, n.9, p. 755-763,
1998.
Eddy, S. R. HMMER – User’s Guide Biological sequence analysis using profile
hidden Markov models. St. Louis, EUA, Instituto Médico Howard Hughes e
Departamento de Genética da Escola de Medicina da Universidade de Washington,
98p., Out. 2003. Versão 2.3.2. Disponível em:
ftp://ftp.genetics.wustl.edu/pub/eddy/hmmer/current/userguide.pdf . Acesso em:
Nov. 2003.
Goeman, H.; Clausen, M. A new practical linear space algorithm for the longest
common subsequence problem. Information Processing Letters, v. 75, p. 275-281,
2000.
Gotoh, O. An improved algorithm for matching biological sequences. Journal of
Molecular Biology, n. 162, p. 705-708, 1982.
Grate, L.; Hughey, R.; Karplus, K.; Sjölander, K. Stochastic Modeling Techniques:
Understanding and using hidden Markov models. Santa Cruz, USA,
Universidade da California, 35p, Jun. 1996. Disponível em:
http://www.cse.ucsc.edu/research/compbio/papers/ismb96_tutorial.ps. Acesso em:
Jul. 1999.
Grice, J. A.; Hughey, R.; Speck, D. Parallel sequence alignment in limited space. In:
Int. Conf. Intelligent Systems for Molecular Biology, 1995, Cambridge.
Proceedings. AAAI/MIT Press, 1995. p. 145-153.
Grice, J. A.; Hughey, R.; Speck, D. Reduced space sequence alignment. CABIOS, v.
13, n. 1, p. 45-53, 1997.
Haussler, D.; Krogh, A.; Mian, I. S.; Sjölander, K. Protein Modeling using Hidden
Markov Models: Analysis of Globins. In: 26th Hawaii International Conference on
System Sciences, 26., Honolulu, 1993. Proceedings. Los Alamitos: IEEE Computer
Society Press, p. 792-802. 1993.
202
Hirschberg, D. S. A linear space algorithm for computing maximal common
subsequences. Communications of the ACM, v. 18, n. 6, p. 341-343, 1975.
Hirschberg, D. S. Algorithms for the Longest Common Subsequence Problem. Journal
of the ACM, v. 24, n. 4, p. 664-675, 1977.
Hughey, R.; Krogh, A. Hidden Markov models for sequence analysis: extension e
analysis of the basic method. CABIOS, v. 12, n. 2, p. 95-107, 1996.
Hughey, R.; Karplus, K.; Krogh, A. SAM – Sequence Alignment and Modeling
Software System. Santa Cruz, USA: Centro Baskin para Ciência e Engenharia de
Computação da Universidade da California. 168p., Jul. 2003. Versão 3.4. (UCSC-
CRL-99-11). Disponível em:
http://www.cse.ucsc.edu/research/compbio/papers/sam_doc.pdf. Acesso em: Jul.
2003.
Hunt, J. W.; Szymanski, T. G. A fast algorithm for computing longest common
subsequence. Communications of the ACM, v. 20, n. 5, p. 350-353, 1977.
Human Genome Management Information System (HGMIS). Primer on Molecular
Genetics. Washington, DC: U.S. Department of Energy Human Genome Program,
1992. Disponível em:
http://genome.gsc.riken.go.jp/hgmis/publicat/primer/primer.pdf. Acesso em: Jul.
1999.
Human Genome Management Information System (HGMIS). Genomics and Its
Impact on Medicine and Society - A 2001 Primer. Washington, DC: U.S.
Department of Energy Human Genome Program, 2001. Disponível em:
http://genome.gsc.riken.go.jp/hgmis/publicat/primer2001/primer11.pdf. Acesso em:
Jun. 2001.
Karplus, K.; Barret, C.; Hughey, R. Hidden Markov Models for Detecting Remote
Protein Homologies. Bioinformatics, v.14, n.10, p. 846-856, 1998.
203
Kearney, J. K.; Sedlmeyer, R. L.; Thompson, W. B.; Gray, M. A.; Adler, M. A.
Software Complexity Measurement. Communications of the ACM, v. 29, n. 11, p.
1044-1050, 1986.
Kehagias, A. Bayesian Classification of Hidden Markov Models. Math. Comput.
Modelling, v. 23, n. 5, p. 25-43, 1996.
Knuth, D. The Art of Computer Programming: Fundamental Algorithms.
Massachusetts: Addison-Wesley, 1973.
Koski, A. Modelling ECG signals with hidden Markov models. Artificial Intelligence
in Medicine, n. 8, p.453-471, 1996.
Koski, T. A Course on Hidden Markov Models: with applications to
bioinformatics. Faculdade de estatística e Matemática do KTH Instituto Real de
Tecnologia, Estocolmo, Suecia. 327p., Mar. 1999. Disponível em:
http://www.math.kth.se/matstat/users/timo/komp.html. Acesso em: Out. 2000.
Kowalski, R. Algorithm = Logic + Control. Communications of the ACM, v. 22, n. 7,
p. 424-436, 1979.
Krogh, A.; Brown, M.; Mian, I. S.; Sjölander, K.; Haussler, D. Hidden Markov
Models in Computational Biology: applications to protein modeling. Santa Cruz,
USA, Laboratórios Sinsheimer de Computação e Ciência da Informação da
Universidade da California, 35p., Set. 1993. Disponível em:
http://www.cse.ucsc.edu/research/compbio/papers/sam_doc.pdf . Acesso em: Out.
2003.
Leeuwen, J. van; Wiedermann, J. The turing machine paradigm in contemporary
computing. Utrecht, Netherlands: Departamento de Ciência da Computação da
Universidade de Utrecht. 19p., 2000. (UU-CS-2000-33). Disponível em:
http://www.archive.cs.uu.nl/pub/ruu/cs/techreps/cs-2000/2000-33.pdf. Acesso em:
Abr. 2004.
204
Ma, X.; Shi, J. A New Method for Discrimination Between Fault and Magnetzing Irush
Current Using HMM. Eletric Power System Research, N. 56, p.43-49, 2000.
Myers, E. W. An O(nd) difference algorithm and its variations. Algorithmica, n. 1, p.
251-266, 1986.
Myers, E. W.; Miller, W. Optimal alignments in linear space. CABIOS, v. 4, n. 1, p.
11-17, 1988.
Myers, E. W. An overview of sequence comparison algorithms in molecular biology.
Tucson, AZ, USA: Departamento de Ciência da Computação, Universidade do
Arizona, 25 p., Dez. 1991. Disponível em:
http://www.cs.arizona.edu/people/gene/papers/compbio.survey.ps. Acesso em: Jul.
2002.
Needleman, S. B.; Wunsch, C. D. A general method applicable to the search for
similarities in the amino acid sequence of two proteins. Journal of Molecular
Biology, n. 48, p. 443-453, 1970.
Net-ID, Inc. HMMpro. Copyright Net-ID Inc., 1998. Ver. 2.2.1 (B47). Disponível em:
http://www.netid.com. Acesso em: Abr. de 2003.
Pedersen, C. N. S. Algorithms in Computational Biology. Aarhus, 210 p. Tese
(Doutorado em Ciência da Computação) – Faculty of Science of the University of
Aarhus, 2000.
Powell, D. R.; Allison, L.; Dix, T. I. A versatile divide and conquer technique for
optimal string alignment. Information Processing Letters, n. 70, p. 127-139, 1999.
Rabiner, L. R. A tutorial on hidden Markov models and selected applications in speech
recognition. Proceedings of the IEEE, v. 77, n. 2, p. 257-286, Fev. 1989.
Resende, P.J.; Stolfi, J. Fundamentos de Geometria Computacional. Recife: IX
Escola de Computação – UFPE, 1994.
205
Rick, C. A New Flexible Algorithm for the Longest Common Subsequence Problem.
In: 6th Symposium on Combinatorial Pattern Matching (CPM'95), Espoo, Finland,
1995. Proceedings. Lecture Notes in Computer Science, v. 937, p. 340-351, 1995.
Disponível em: http://theory.cs.uni-bonn.de/blum/mitarbeiter/rick/lcs.dvi. Acesso
em: Jan. 2003.
Rick, C. Simple and fast linear space computation of longest common subsequences.
Information Processing Letters, v. 75, n. 6, p. 275-281, 2000.
Sato, M.; Sugaya, N.; Murakami, H.; Aburatani, S.; Horimoto, K. Profile-Profile
Comparison Based on Hidden Markov Models. Genome Informatics, v. 14, p. 522-
523, 2003.
Smith, T. F.; Waterman, M. F. Identification of common molecular subsequences.
Journal of Molecular Biology, n. 147, p. 195-197, 1981.
Swiss-Prot group. Swiss-Prot protein knowledgebase release 43.0 statistics. Geneva:
Swiss Institute of Bioinformatics, 2004.
Tarnas, C.; Hughey, R. Reduced space hidden Markov model training. Bioinformatics,
v. 14, n. 5, p. 401-406, 1998.
Toscani, L. V.; Veloso, P. A. S. Complexidade de Algoritmos. Porto Alegre: Instituto
de Informática da UFRGS: Editora Sagra Luzzatto, 2001. 202p.
Wheeler, R.; Hughey, R. Optimizing Reduced-Space Sequence Analysis.
Bioinformatics, v. 16, n. 12, p. 1082-1090, 2000.
Wu, S; Mamber, U.; Myers, G; Miller, W. An ( )npO sequence comparison algorithm.
Inform. Process. Lett., v. 35, p. 317-323, 1990.
206
BIBLIOGRAFIA COMPLEMENTAR
Dunmur, A. P.; Titterington, D. M. The influence of initial conditions on maximum
likelihood estimation of the parameters of a binary hidden Markov model. Statistics
& Probability Letters, v. 40, p. 67-73, 1998.
Golding, B. Course BIOL721 – Elementary Sequence Analysis. Hamilton, Ontario,
Canadá, Departamento de Biologia da Universidade McMaster, 198 p., 2003.
Disponível em: http://helix.biology.mcmaster.ca/4k03.pdf . Acesso em: Jun. 2003.
Hughes, J. P. Computing the observed information in the hidden Markov model using
the EM algorithm. Statistics & Probability Letters, v. 32, p. 107-114, 1997.
Ikeda, S. Acceleration of the EM algorithm. Systems and Computers in Japan, v. 31,
n. 2, p. 10-18, 2000.
Karchin, R. Hidden Markov Models and Protein Sequence Analysis. Santa Cruz,
USA, Centro Baskin para Ciência da Computação e Engenharia da Universidade da
California, 1998, 18 p. Disponível em:
http://www.cse.ucsc.edu/research/compbio/ismb99.handouts/kk185fp.html. Acesso
em: Set. 2000.
Karchin, R.; Hughey, R. Weighting hidden Markov models for maximum
discrimination. Bioinformatics, v. 14, n. 9, p. 772-782, 1998.
Lin, G. H.; Ma, B.; Zhang, K. Edit distance between two RNA structures. In: 5a
Conferência Anual Internacional em Biologia Computacional (RECOMB).
Proccedings. New York, NY, USA, ACM Press, p. 211-220, 2001.
McClure, M. A. Parameterization studies for the SAM and HMMER methods of
Hidden Markov Model generation. In: Fourth International Conference on
Intelligent Systems for Molecular Biology. Proceedings. Menlo Park, CA, USA,
AAAI Press, p.155-164, 1996.
207
Minka, T. P. From hidden Markov models to linear dynamical systems. Cambridge,
MA, USA: MIT Media Lab, 10 p., July 1999. (MIT-TR-531). Disponível em:
ftp://whitechapel.media.mit.edu/pub/tech-reports/tr-531.pdf. Acesso em: Set. 2002.
Morgenstern, B. A space-efficient algorithm for aligning large genomic sequences.
Bioinformatics, v. 16, n. 10, p. 948-949, 2000.
Murphy, K. P. Kevin Murphy. Learning Markov Process. The Encyclopedia of
Cognitive Science, Nadel et al. (eds), Nature Macmillan, 2002. No prelo.
Myers, E. W.; Miller, W. Optimal alignments in linear space. CABIOS, v. 4, n. 1, p.
11-17, 1998.
Rydén, T. On recursive estimation for hidden Markov models. Stochastic Process and
their applications, v. 66, p. 79-96, 1997.
Schneider, T. D. Information Theory Primer. Laboratório de Biologia Experimental e
Computacional, Centro Frederick de Desenvolvimento e Pesquisa do Cancer do
Instituto Nacional do Cancer, USA. Jan. 2003, 14 p. Disponível em:
http://www.lecb.ncifcrf.gov/~toms/paper/primer/latex/paper.pdf. Aceso em: Mai.
2003.
Shatkay, H.; Kaelbling, L. P. Learning hidden Markov models with geometric
information. Providence, Rhode Island: Departamento de Ciência da Computação
da Universidade Brown, 1997. 16 p. (CS- 97-04). Disponível em:
ftp://ftp.cs.brown.edu/pub/techreports/97/cs97-04.ps.z. Acesso em: Jan. 2001.
Sin, B.; Kim, J. H. Nonstationary hidden Markov model. Signal Processing, v. 46, p.
31-46, 1995.
Tsoka, S.; Ouzounis, C. A. Recent developments and future directions in computational
genomics. FEBS Letters, v. 480, p. 42-48, 2000.
208
APÊNDICE 1
ALGORITMO DE VITERBI BÁSICO
/* **************************************************************************************** * * Algoritmo de Viterbi para alinhar seqüências de aminoácidos * Copyright (C) 2003 José Olimpio Ferreira * Instituto Nacional de Pesquisas Espaciais e * Universidade Católica de Goiás * All Rights Reserved * **************************************************************************************** * * align_viterbi_basic_uni.c * * --> Algoritmo de Viterbi para obter o melhor alinhamento de seqüências de * aminoácidos a um HMM. * --> Este é um algoritmo básico, que necessita de espaço da ordem O(mn). * **************************************************************************************** * * Entradas: * --> Arquivo de probabilidades de transição de estados --> hmm[nos].transicoes * --> Arquivo de probabilidades de emissão de símbolos --> hmm[nos].emissoes * --> Arquivo contendo 1 seqüência para alinhamento ao HMM --> <nome_arquivo> * --> nós = tamnho do HMM * * Saída: * --> Escore e alinhamento Viterbi da seqüência ao HMM --> hmm[nos].ubalign * **************************************************************************************** * * Compilação - Linkedição - Construção do executável -> exemplo para intel pentium 3 celerom * --> gcc -c -march=pentium3 -mcpu=pentium3 -mfpmath=sse -O3 align_viterbi_basic_uni.c * --> gcc -c -march=pentium3 -mcpu=pentium3 -mfpmath=sse -O3 hpctim.c * --> gcc -c -march=pentium3 -mcpu=pentium3 -mfpmath=sse -O3 hpcwall.c * --> gcc -c -march=pentium3 -mcpu=pentium3 -mfpmath=sse -O3 etime.c * --> gcc -o align_viterbi_basic_uni align_viterbi_basic_uni.o hpctim.o hpcwall.o etime.o -lm * * Compilação - Linkedição - Construção do executável -> exemplo para amd athlon k-7 * --> gcc -c -march=athlon -mcpu=athlon -mfpmath=387 -O3 align_viterbi_basic_uni.c * --> gcc -c -march=athlon -mcpu=athlon -mfpmath=387 -O3 hpctim.c * --> gcc -c -march=athlon -mcpu=athlon -mfpmath=387 -O3 hpcwall.c * --> gcc -c -march=athlon -mcpu=athlon -mfpmath=387 -O3 etime.c * --> gcc -o align_viterbi_basic_uni align_viterbi_basic_uni.o hpctim.o hpcwall.o etime.o -lm * **************************************************************************************** */ #include <stdio.h> #include <stdlib.h> #include <math.h> #include <time.h> #include <string.h> /*Definie algumas macros. */ #define IAA(x) (strchr(aa, (x)) - aa) /* Índice de símbolos de aminoácidos. */ #define log2(x) ((x)>0.0 ? log(x)*1.44269504 : -19) /* logaritmo de base 2. */ #define exp2(x) (exp(x)*0.69314718) /* exponencial de base 2. */ #define INFINITO 99999999 /* infinito para log-probilidades de valores zero. */
209
#define aa_alfabeto "ACDEFGHIKLMNPQRSTVWY" /* Alfabeto não-degenerado de aminoácidos. */ /* String contendo os símbolos não-degenerados dos aminoácidos. */ const char aa[21] = aa_alfabeto; /* Índices para as probabilidades de transição de estado do HMM. */ enum transicoes_indices PDD=0,PMD=1,PID=2,PDM=3,PMM=4,PIM=5,PDI=6,PMI=7,PII=8; /* Variáveis: * NS --> número de seqüências. * TS --> tamanho da maior seqüência. * NN --> número de nós do HMM. * Model --> HMM 500,1000,2000,4000,8000 nós. * Arquivo --> nome do arquivo que contém as seqüências no formato HMMpro. */ const long int NS, TS, NN; char Model[80], Arquivo[80]; /* --> Freqüências f(i) - Dayhoff - de ocorrência dos 20 aminoácidos. * (função massa de probabilidade para aminoácidos). * --> Obtidos do BD SwissProt 34: considerando 21.210.388 raidicais. * --> Apresentadas em ordem alfabética por código de letras simples. * --> Usadas como probabilidades de emissão de símbolos no modelo nulo ou aleatório (R). */ const float f_aa[20] = (float)-3.72699742, /* (float)0.075520, /* A */ (float)-5.88061460, /* (float)0.016973, /* C */ (float)-4.23707464, /* (float)0.053029, /* D */ (float)-3.98384032, /* (float)0.063204, /* E */ (float)-4.61663135, /* (float)0.040762, /* F */ (float)-3.86884780, /* (float)0.068448, /* G */ (float)-5.47997107, /* (float)0.022406, /* H */ (float)-4.12572395, /* (float)0.057284, /* I */ (float)-4.07344183, /* (float)0.059398, /* K */ (float)-3.42044908, /* (float)0.093399, /* L */ (float)-5.40696564, /* (float)0.023569, /* M */ (float)-4.46456809, /* (float)0.045293, /* N */ (float)-4.34338099, /* (float)0.049262, /* P */ (float)-4.63554859, /* (float)0.040231, /* Q */ (float)-4.27724022, /* (float)0.051573, /* R */ (float)-3.79157763, /* (float)0.072214, /* S */ (float)-4.12144885, /* (float)0.057454, /* T */ (float)-3.93783407, /* (float)0.065252, /* V */ (float)-6.32042847, /* (float)0.012513, /* W */ (float)-4.96646070 /* (float)0.031985 /* Y */ ; /* Ponteiros para arquivos. */ FILE *seq_in, *pt_in, *pe_in, *align_out, *scores_out; /* Declaração dos protótipos das funções internas. */ int analisa_arq_seq(long int vseq[4]); int read_seq (char As[TS+1]); int read_model (float PT[][9], float mPE[][20], float iPE[][20]); int alinha_viterbi_basico(float PT[][9],float mPE[][20],float iPE[][20],char As[TS+1]); /* Declaração dos protótipos das funções externas. */ void etime(float *result); void hpcwall(double *retval); void hpctim(float *wtime, float *ctime, float *wbegin, float *cbegin); /* FUNÇÃO PRINCIPAL --> MAIN. */ int main (int argc, char *argv[]) long int i, j, vseq[4]=0,0,0,0, *ptr; int status; float *ptrf; /* Trata os argumentos da função principal. */ if(argc!=3) printf("\nVoce esqueceu de informar:\n"); printf("o tamanho do HMM (Opções: 500, 1000, 2000, 4000 e 8000),\n"); printf("o nome do arquivo contendo as seqüências.\n\n"); printf("Entre: <nome_programa> <tamanho_HMM> <nome_arquivo>\n"); printf("O arquivo deve estar no formato HMMpro --> http://www.netid.com\n"); exit(1); /* Inicializa algumas variáveis globais do tipo char* e const int. Usadas como informações de nomes de arquivos, de tamnho do modelo e de dimesões de matrizes. */
210
strcpy(Model,argv[1]); (const) ptr = &NN; *ptr = atoi(argv[1]); strcpy(Arquivo,argv[2]); /* Chamada para a função análise do arquivo de seqüências. */ status = analisa_arq_seq(vseq); /* Verifica o status do retorno da função analisa_arq_seq. */ if(status!=1) printf("Erro na análise do arquivo de sequecias.\n"); return(0); (const) ptr = &NS; *ptr = vseq[0]; (const) ptr = &TS; *ptr = vseq[1]; /* Matriz usada para armazenar a seqüência a ser alinhada. */ char strAs[TS+1]; /* Matriz de probabilidades de transição do HMM.*/ float pt_hmm[NN+2][9]; for(i=0;i<NN+2;i++) for(j=0;j<9;j++) pt_hmm[i][j] = (float)0.0; /* Matriz de probabilidades de emissão de símbolos dos estados MATCH. */ float mf_aa[NN][20]; for(i=0;i<NN;i++) for(j=0;j<20;j++) mf_aa[i][j] = 0.0; /* Matriz de probabilidades de emissão de símbolos dos estados INSERT. */ float if_aa[NN+1][20]; for(i=0;i<NN+1;i++) for(j=0;j<20;j++) if_aa[i][j] = 0.0; /* Chamada para a função de leitura da seqüência. */ status = read_seq(strAs); /* Verifica o status do retorno da função read_seq. */ if(status!=2) printf("Erro na leitura do arquivo de sequecias\n"); return(0); /* Chamada para a função de leitura de parâmetros do HMM. */ status = read_model(pt_hmm, mf_aa, if_aa); /* Verifica o status do retorno da função read_model. */ if(status!=3) printf("Erro na leitura do arquivo de parâmetros do HMM\n"); return(0); /* Converte probabilidades para log-probabilidades. */ for(i=0;i<NN+2;i++) for(j=0;j<9;j++) pt_hmm[i][j] = log2(pt_hmm[i][j]); for(i=0;i<NN;i++) for(j=0;j<20;j++) if_aa[i][j] = log2(if_aa[i][j]); for(j=0;j<20;j++) mf_aa[i][j] = log2(mf_aa[i][j]); for(j=0;j<20;j++) if_aa[i][j] = log2(if_aa[i][j]); /* Chamada para a função de alinhamento de seqüências ao HMM. */ status = alinha_viterbi_basico(pt_hmm, mf_aa, if_aa, strAs); /* Verifica o status do retorno da função alinha_viterbi_basico. */ if(status!=4) printf("Erro no alinhamento da seqüência ao HMM\n"); return(0); /* Término da função principal --> retorno para o SO. */ return (0); /* FUNÇÃO ANALISA ARQUIVO DE SEQÜÊNCIAS. */ int analisa_arq_seq(long int vseq[4]) int i,j,k; long int count=0, soma=0, max=2147483647, maximo=0,minimo=2147483647, media=0; char string[1024000]; /* Abindo arquivo de seqüências para leitura. */ seq_in=fopen(Arquivo,"r"); if(seq_in==NULL) perror("Erro abrindo arquivo de seqüências.\n"); exit(0); /* Verificando o cabecalho de formato do arquivo. */ fgets(string,max,seq_in); if(j=strcmp(string,"ObjectData:\n")) printf("(1) Problemas no formato do arquivo --> (%d).\n",j); exit(1); fgets(string,max,seq_in); if(j=strcmp(string,"DATA: string\n")) printf("(2) Problemas no formato do arquivo --> (%d).\n",j); exit(1); fgets(string,max,seq_in); if(j=strcmp(string,"\n")) printf("(3) Problemas no formato do arquivo --> (%d).\n",j); exit(1); i=0;j=1; /* Obendo estatÍsticas das seqüências. */ while(j) fgets(string,max,seq_in); if(string[0]!='\n' && string[0]!='\r' && string[0]!=' ' && !feof(seq_in)) count = strlen(string)-1; soma += count; if(count>maximo) maximo=count;
211
if(count<minimo && count!=1) minimo=count; i++; string[0]=' '; else j=0; fprintf(stdout,"O arquivo de seqüências contém: NS = %d, TS (médio) = %d, NÓs do HMM = %s.\n",i,maximo,Model); media = (int) soma/i; vseq[0] = i; vseq[1] = maximo+1; vseq[2] = minimo; vseq[3] = media; return(1); /* FUNÇÃO DE LEITURA DA SEQÜÊNCIA. */ int read_seq (char As[TS+1]) int i,j; char string[TS+1]; /* Reinicializa o indicador de posição do arquivo. */ rewind(seq_in); if(seq_in==NULL) perror("Erro reinicializando indicador de posição\n"); exit(0); /* Copiando a seqüência do arquivo para uma matriz de strings. */ for(i=0;i<3;i++) fgets(string,TS+1,seq_in); fgets(As,TS+1,seq_in); j=(int)strlen(As); As[j-1]='\0'; /* Fechado o arquivo de seqüências */ if(fclose(seq_in)!=0) perror("Erro fechando arquivo de seqüências."); exit(0); return(2); /* FUNÇÃO DE LEITURA DOS PARÂMETROS DO MODELO. */ int read_model (float PT[][9], float mPE[][20], float iPE[][20]) char nome_arquivo[20] = "hmm", string[TS+1]; long int i, j, state; /* Abre arquivo de probabilidades de transições de estados para leitura. */ strcat(nome_arquivo, Model); strcat(nome_arquivo, ".transicoes"); pt_in=fopen(nome_arquivo,"r"); if(pt_in==NULL) perror("Erro abrindo arquivo de probabiledades de transições do HMM.\n"); exit(0); /* Copiando as probabilidades do arquivo para a matriz de probabilidades de transições. */ /* Lendo as 2 primeiras linhas do arquivo (bloco 0 do HMM) e preenchendo as 2 primeiras linhas da matriz. */ fscanf(pt_in,"%d%f%f%f",&state,&PT[1][4],&PT[0][7],&PT[1][1]); fscanf(pt_in,"%d%f%f%f",&state,&PT[1][5],&PT[0][8],&PT[1][2]); /* Lendo as próximas 3*NN-2 linhas do arquivo (blocos de 1 a NN-1 do HMM) e preenchendo as próximas NN-1 linhas da matriz. */ for(i=1;i<NN;i++) fscanf(pt_in,"%d%f%f%f",&state,&PT[i+1][3],&PT[i][6],&PT[i+1][0]); fscanf(pt_in,"%d%f%f%f",&state,&PT[i+1][4],&PT[i][7],&PT[i+1][1]); fscanf(pt_in,"%d%f%f%f",&state,&PT[i+1][5],&PT[i][8],&PT[i+1][2]); /* Lendo as 2 primeiras linhas do arquivo e preenchendo as 2 primeiras linhas da matriz. */ fscanf(pt_in,"%d%f%f",&state,&PT[NN+1][3],&PT[NN][6]); fscanf(pt_in,"%d%f%f",&state,&PT[NN+1][4],&PT[NN][7]); fscanf(pt_in,"%d%f%f",&state,&PT[NN+1][5],&PT[NN][8]); /* Fechado o arquivo da seqüência. */ if(fclose(pt_in)!=0) perror("Erro fechando arquivo de probabilidade de transições."); exit(0); /* Abre arquivo de probabilidades de emissões de símbolos para leitura. */ strcpy(nome_arquivo,"hmm\0"); strcat(nome_arquivo, Model); strcat(nome_arquivo, ".emissoes"); pe_in=fopen(nome_arquivo,"r"); if(pe_in==NULL) perror("Erro abrindo arquivo de probabiledades de emissões do HMM.\n"); exit(0); /* Copiando as probabilidades do arquivo para a matriz de probabilidades de emissões. */ for(i=0;i<NN;i++) fscanf(pe_in,"%d",&state); for(j=0;j<20;j++) fscanf(pe_in,"%f",&iPE[i][j]); fscanf(pe_in,"%d",&state);
212
for(j=0;j<20;j++) fscanf(pe_in,"%f",&mPE[i][j]); fscanf(pe_in,"%d",&state); for(j=0;j<20;j++) fscanf(pe_in,"%f",&iPE[NN][j]); /* Fechado o arquivo de seqüências. */ if(fclose(pe_in)!=0) perror("Erro fechando arquivo de probabilidades emissões."); exit(0); return(3); /* FUNÇÃO ALINHA SEQÜÊNCIA --> ALGORITMO DE VITERBI BÁSICO */ int alinha_viterbi_basico(float PT[][9],float mPE[][20],float iPE[][20],char As[TS+1]) long int s, t, b, aTS; char nome_arquivo[20] = "hmm"; float logodds, escore, max, aux1, aux2; int tcbk=0, teste=0, origem=0; /* Variáveis usadas para a medição de tempo. */ float wtimes=0.0, ctimes=0.0, wbegins=0.0, cbegins=0.0; aTS=strlen(As); /* Matriz de escores de Viterbi. */ struct matriz_PD float del, mat, ins; /* Guarda os escores dos estados delete, insert e match. */ int tb[3]; /* Guarda o estado anterior no caminho Viterbi: td=0, ti=1 e tm=2. */ align[NN+1][aTS+1]; /* Inicializa rotina de tempo. */ hpctim(&wtimes,&ctimes,&wbegins,&cbegins); hpctim(&wtimes,&ctimes,&wbegins,&cbegins); /* Inicializa a matriz de escores de Viterbi. */ for(b=0;b<NN+1;b++) for(t=0;t<aTS+1;t++) align[b][t].del = 0.0; align[b][t].mat = 0.0; align[b][t].ins = 0.0; align[b][t].tb[0] = 11; align[b][t].tb[1] = 11; align[b][t].tb[2] = 11; /* Abre arquivo para escrita do alinhamento. */ strcat(nome_arquivo, Model); strcat(nome_arquivo, ".ubalign"); align_out = fopen(nome_arquivo,"w"); if(align_out == NULL) perror("Erro abrindo arquivo de escrita do alinhamento ao HMM.\n"); exit(0); /* Efetua o alinhamento da seqüência ao modelo e escreve este para o arquivo. */ fprintf(align_out,"Algoritmo de Viterbi Básico.\n"); fprintf(align_out,"Alinhamento da seqüência com %d radicais a um HMM com %s nós.\n",aTS,Model); /* Efetua o cálculo da memória mínima requerida */ aux1=(float) 18*NN*aTS; aux2=aux1/1024; max=aux2/1024; fprintf(align_out,"A memória mínima alocada é de %f bytes (= %f Kb = %f Mb).\n",aux1,aux2,max); printf("A memória mínima alocada é de %f bytes (= %f Kb = %f Mb.)\n",aux1,aux2,max); /* Efetua a tomada de tempo no início do alinhameto da seqüência. */ hpctim(&wtimes, &ctimes, &wbegins, &cbegins); /* Enche a linha 0 da matriz de Viterbi para a seqüência s (somente insert). */ /* Primeiro Enche as células 0 e 1. */ align[0][0].del=(float)-INFINITO; align[0][0].tb[0]=10; align[0][0].mat=0.0; align[0][0].tb[1]=9; align[0][0].ins=(float)-INFINITO; align[0][0].tb[2]=10; align[0][1].del=(float)-INFINITO; align[0][1].tb[0]=10; align[0][1].mat=(float)-INFINITO; align[0][1].tb[1]=10; align[0][1].ins=iPE[0][IAA(As[0])]-f_aa[IAA(As[0])]+PT[0][PMI]; align[0][1].tb[2]=9; /* Depois as células de t=2 até aTS da linha 0. */ for(t=2;t<aTS+1;t++) align[0][t].del=(float)-INFINITO; align[0][t].tb[0]=10; align[0][t].mat=(float)-INFINITO; align[0][t].tb[1]=10; align[0][t].ins=iPE[0][IAA(As[t-1])]-f_aa[IAA(As[t-1])]+align[0][t-1].ins+PT[0][PII]; align[0][t].tb[2]=PII; /* Enche as demais linhas da matriz de Viterbi para a seqüência s. */ for(b=1;b<NN+1;b++) /* Preenche a célula 0 da linha b (somente delete). */ if(b==1) align[b][0].del=align[b-1][0].mat+PT[b][PMD]; align[b][0].tb[0]=9; else align[b][0].del=align[b-1][0].del+PT[b][PDD]; align[b][0].tb[0]=PDD; align[b][0].mat=(float)-INFINITO; align[b][0].tb[1]=10;
213
align[b][0].ins=(float)-INFINITO; align[b][0].tb[2]=10; /* Preenche as demais células da linha b. */ for(t=1;t<aTS+1;t++) /* Preenche o campo delete da célula t da linha b. */ max=align[b-1][t].del+PT[b][PDD]; align[b][t].tb[0]=PDD; aux1=align[b-1][t].mat+PT[b][PMD]; aux2=align[b-1][t].ins+PT[b][PID]; if(max<=aux1) max=aux1; align[b][t].tb[0]=PMD; if(max<aux2) max=aux2; align[b][t].tb[0]=PID; align[b][t].del=max; /* Preenche o campo match da célula t da linha b. */ max=align[b-1][t-1].del+PT[b][PDM]; align[b][t].tb[1]=PDM; aux1=align[b-1][t-1].mat+PT[b][PMM]; aux2=align[b-1][t-1].ins+PT[b][PIM]; if(max<=aux1) max=aux1; if(b==1) align[b][t].tb[1]=9; else align[b][t].tb[1]=PMM; if(max<aux2) max=aux2; align[b][t].tb[1]=PIM; align[b][t].mat=mPE[b-1][IAA(As[t-1])]-f_aa[IAA(As[t-1])]+max; /* Preenche o campo insert da célula t da linha b. */ max=align[b][t-1].del+PT[b][PDI]; align[b][t].tb[2]=PDI; aux1=align[b][t-1].mat+PT[b][PMI]; aux2=align[b][t-1].ins+PT[b][PII]; if(max<=aux1) max=aux1; align[b][t].tb[2]=PMI; if(max<aux2) max=aux2; align[b][t].tb[2]=PII; align[b][t].ins=iPE[b][IAA(As[t-1])]-f_aa[IAA(As[t-1])]+max; /* Conclui a fase forward --> Preenche o campo match da célula t da linha b. */ max=align[b-1][t-1].del+PT[b][PDM]; origem=PDM; aux1=align[b-1][t-1].mat+PT[b][PMM]; aux2=align[b-1][t-1].ins+PT[b][PIM]; if(max<=aux1) max=aux1; origem=PMM; if(max<aux2) max=aux2; origem=PIM; logodds=max; escore=exp2(logodds); fprintf(align_out,"Escore Viterbi: seqüência com %d radicais -> %f (log-odds = %f).\n",aTS,escore,logodds); /* Fase de retrocedimento sobre a matriz de Viterbi para recuperar o melhor caminho * Usar a função scanf() para imprimir os símbolos numa matriz (string) de char e * depois fazer as manipulações necessarias para imprimir corretamente num arquivo. */ b=NN+1; t=aTS+1; tcbk=origem; while(tcbk!=9) teste=tcbk; if(teste==PDD || teste==PMD || teste==PID) b--; if(teste==PDD) fprintf(align_out,"D%d(-) ",b); tcbk=align[b][t].tb[0]; else if(teste==PMD) fprintf(align_out,"M%d(%c) ",b,As[t-1]); tcbk=align[b][t].tb[1]; else fprintf(align_out,"I%d(%c) ",b,As[t-1]); tcbk=align[b][t].tb[2]; if(teste==PDM || teste==PMM || teste==PIM) b--; t--; if(teste==PDM) fprintf(align_out,"D%d(-) ",b); tcbk=align[b][t].tb[0]; else if(teste==PMM) fprintf(align_out,"M%d(%c) ",b,As[t-1]); tcbk=align[b][t].tb[1]; else fprintf(align_out,"I%d(%c) ",b,As[t-1]); tcbk=align[b][t].tb[2]; if(teste==PDI || teste==PMI || teste==PII) t--; if(teste==PDI) fprintf(align_out,"D%d(-) ",b); tcbk=align[b][t].tb[0]; else if(teste==PMI) fprintf(align_out,"M%d(%c) ",b,As[t-1]); tcbk=align[b][t].tb[1]; else fprintf(align_out,"I%d(%c) ",b,As[t-1]); tcbk=align[b][t].tb[2]; fprintf(align_out,"\n"); /* Efetua a tomada de tempo no final do alinhameto da seqüência. */ hpctim(&wtimes, &ctimes, &wbegins, &cbegins); printf("WALL time gasto no alinhamento --> %f.\n", wtimes); printf("CPU time gasto no alinhamento --> %f.\n", ctimes);
214
fprintf(align_out,"WALL time gasto no alinhamento --> %f.\n",wtimes); fprintf(align_out,"CPU time gasto no alinhamento --> %f.\n",ctimes); /* Fechado o arquivo de escrita do escore e do alimhamento da seqüência. */ if(fclose(align_out)!=0) perror("Erro fechando arquivo de escrita do alinhamento."); exit(0); return(4);
215
216
APÊNDICE 2
ALGORITMO DE VITERBI COM DE 2-NÍVEIS CHECKPOINTS POR LINHAS E PARTICIONAMENTO MÓVEL DE MEMÓRIA
/*
* * Algoritmo de Viterbi para alinhar seqüências de aminoácidos * Copyright (C) 2003 José Olimpio Ferreira * Instituto Nacional de Pesquisas Espaciais e * Universidade Católica de Goiás * All Rights Reserved * **************************************************************************************** * * align_viterbi_ckpts_line_uni.c * * --> Algoritmo de Viterbi para obter o melhor alinhamento de seqüências de aminoácidos a um HMM. * --> Este é um algoritmo de 2-níveis com checkpoints por linhas (padrão) * --> Requer espaço de ordem O(m*SQRT(2*n)). * **************************************************************************************** * * Entradas: * --> Arquivo de probabilidades de transição de estados --> hmm[nos].transições * --> Arquivo de probabilidades de emissão de símbolos --> hmm[nos].emissões * --> Arquivo de seqüências para alinhamento ao HMM --> <nome_arquivo> * --> nós = tamnho do HMM * * Saída: * --> Escore e alinhamento Viterbi da seqüência ao HMM --> hmm[nos].urclalign * * Compilação - Linkedição - Construção do executável -> exemplo para intel pentium 3 celerom * --> gcc -c -march=pentium3 -mcpu=pentium3 -mfpmath=sse -O3 align_viterbi_ckpts_line_uni.c * --> gcc -c -march=pentium3 -mcpu=pentium3 -mfpmath=sse -O3 hpctim.c * --> gcc -c -march=pentium3 -mcpu=pentium3 -mfpmath=sse -O3 hpcwall.c * --> gcc -c -march=pentium3 -mcpu=pentium3 -mfpmath=sse -O3 etime.c * --> gcc -o align_viterbi_ckpts_line_uni align_viterbi_ckpts_line_uni.o hpctim.o hpcwall.o etime.o -lm * * Compilação - Linkedição - Construção do executável -> exemplo para amd athlon k-7 * --> gcc -c -march=athlon -mcpu=athlon -mfpmath=387 -O3 align_viterbi_ckpts_line_uni.c * --> gcc -c -march=athlon -mcpu=athlon -mfpmath=387 -O3 hpctim.c * --> gcc -c -march=athlon -mcpu=athlon -mfpmath=387 -O3 hpcwall.c * --> gcc -c -march=athlon -mcpu=athlon -mfpmath=387 -O3 etime.c * --> gcc -o align_viterbi_ckpts_line_uni align_viterbi_ckpts_line_uni.o hpctim.o hpcwall.o etime.o -lm * **************************************************************************************** */ #include <stdio.h> #include <stdlib.h> #include <math.h> #include <time.h> #include <string.h> /*definie algumas macros. */ #define IAA(x) (strchr(aa, (x)) - aa) /* Índice de símbolos de aminoácidos. */ #define log2(x) ((x)>0.0 ? log(x)*1.44269504 : -19) /* logaritmo de base 2. */ #define exp2(x) (exp(x)*0.69314718) /* exponencial de base 2. */ #define INFINITO 99999999 /* infinito para log-probilidades de valores zero. */ #define aa_alfabeto "ACDEFGHIKLMNPQRSTVWY" /* Alfabeto não-degenerado de aminoácidos. */ /* String contendo os símbolos não-degenerados dos aminoácidos - Alfabeto de aminoácidos. */
217
const char aa[21] = aa_alfabeto; /* Índices para as probabilidades de transição de estado do HMM. */ enum transicoes_indices PDD=0,PMD=1,PID=2,PDM=3,PMM=4,PIM=5,PDI=6,PMI=7,PII=8; /* Variáveis: * NS --> número de seqüências. * TS --> tamanho da maior seqüência. * NN --> número de nós do HMM. * Model --> HMM com 500,1000,2000,4000,8000 nós. * Arquivo --> nome do arquivo que contém as seqüências no formato HMMpro. */ const long int NS, TS, NN; char Model[80], Arquivo[80]; /* --> Freqüências f(i) - Dayhoff - de occorrência dos 20 aminoácidos * (função massa de probabilidade para aminoácidos). * --> Obtidos do BD SwissProt 34: considerando 21.210.388 raidicais. * --> Apresentadas em ordem alfabetica por código de letras simples. * --> Usadas como probabilidades de emissão de símbolos no modelo nulo ou aleatório (R). */ const float f_aa[20] = (float)-3.72699742, /* (float)0.075520, /* A */ (float)-5.88061460, /* (float)0.016973, /* C */ (float)-4.23707464, /* (float)0.053029, /* D */ (float)-3.98384032, /* (float)0.063204, /* E */ (float)-4.61663135, /* (float)0.040762, /* F */ (float)-3.86884780, /* (float)0.068448, /* G */ (float)-5.47997107, /* (float)0.022406, /* H */ (float)-4.12572395, /* (float)0.057284, /* I */ (float)-4.07344183, /* (float)0.059398, /* K */ (float)-3.42044908, /* (float)0.093399, /* L */ (float)-5.40696564, /* (float)0.023569, /* M */ (float)-4.46456809, /* (float)0.045293, /* N */ (float)-4.34338099, /* (float)0.049262, /* P */ (float)-4.63554859, /* (float)0.040231, /* Q */ (float)-4.27724022, /* (float)0.051573, /* R */ (float)-3.79157763, /* (float)0.072214, /* S */ (float)-4.12144885, /* (float)0.057454, /* T */ (float)-3.93783407, /* (float)0.065252, /* V */ (float)-6.32042847, /* (float)0.012513, /* W */ (float)-4.96646070 /* (float)0.031985 /* Y */ ; /* Ponteiros para arquivos. */ FILE *seq_in, *pt_in, *pe_in, *align_out, *scores_out; /* Declaração dos prototipos das funções internas. */ int analisa_arq_seq(long int vseq[4]); int read_seq (char As[TS+1]); int read_model (float PT[][9], float mPE[][20], float iPE[][20]); int viterbi_L2_ckpt_line(float PT[][9],float mPE[][20],float iPE[][20],char As[TS+1]); /* Declaração dos prototipos das funções externas. */ void etime(float *result); void hpcwall(double *retval); void hpctim(float *wtime, float *ctime, float *wbegin, float *cbegin); /* FUNÇÃO PRINCIPAL --> MAIN. */ int main (int argc, char *argv[]) long int i, j, vseq[4]=0,0,0,0, *ptr; int status; float *ptrf; /* Trata os argumentos da função principal. */ if(argc!=3) printf("\nVoce esqueceu de informar:\n"); printf("o tamanho do HMM (Opções: 500, 1000, 2000, 4000 e 8000).\n"); printf("o nome do arquivo contendo as seqüências.\n\n"); printf("Entre: <nome_programa> <tamanho_HMM> <nome_arquivo>.\n"); printf("O arquivo deve estar no formato HMMpro --> http://www.netid.com.\n"); exit(1); /* Inicializa algumas variáveis globais do tipo char* e const int. Usadas como informações de nomes de arquivos, de tamnho do modelo e de dimensões de matrizes. */ strcpy(Model,argv[1]); (const) ptr = &NN; *ptr = atoi(argv[1]); strcpy(Arquivo,argv[2]); /* Chamada para a função analise das seqüências */
218
status = analisa_arq_seq(vseq); /* Verifica o status do retorno da função analisa_arq_seq */ if(status!=1) printf("Erro na analise das sequecias.\n"); return(0); (const) ptr = &NS; *ptr = vseq[0]; (const) ptr = &TS; *ptr = vseq[1]; /* Matriz usada para armazenar as seqüências a serem alinhadas. */ char strAs[TS+1]; /* Matriz de probabilidades de transição do HMM. */ float pt_hmm[NN+2][9]; for(i=0;i<NN+2;i++) for(j=0;j<9;j++) pt_hmm[i][j] = (float)0.0; /* Matriz de probabilidades de emissão de símbolos dos estados MATCH. */ float mf_aa[NN][20]; for(i=0;i<NN;i++) for(j=0;j<20;j++) mf_aa[i][j] = 0.0; /* Matriz de probabilidades de emissão de símbolos dos estados INSERT. */ float if_aa[NN+1][20]; for(i=0;i<NN+1;i++) for(j=0;j<20;j++) if_aa[i][j] = 0.0; /* Chamada para a função de leitura da seqüência. */ status = read_seq(strAs); /* Verifica o status do retorno da função read_seq. */ if(status!=2) printf("Erro na leitura da sequecia.\n"); return(0); /* Chamada para a função de leitura de parametros do HMM. */ status = read_model(pt_hmm, mf_aa, if_aa); /* Verifica o status do retorno da função read_model. */ if(status!=3) printf("Erro na leitura dos parametros do HMM.\n"); return(0); /* Converte probabilidades para log-probabilidades. */ for(i=0;i<NN+2;i++) for(j=0;j<9;j++) pt_hmm[i][j] = log2(pt_hmm[i][j]); for(i=0;i<NN;i++) for(j=0;j<20;j++) if_aa[i][j] = log2(if_aa[i][j]); for(j=0;j<20;j++) mf_aa[i][j] = log2(mf_aa[i][j]); for(j=0;j<20;j++) if_aa[i][j] = log2(if_aa[i][j]); /* Chamada para a função de alinhamento da seqüência ao HMM. */ status = viterbi_L2_ckpt_line(pt_hmm, mf_aa, if_aa, strAs); /* Verifica o status do retorno da função viterbi_L2_ckpt_line. */ if(status!=4) printf("Erro no alinhamento da seqüência ao HMM.\n"); return(0); /* Término da função principal --> retorno para o SO */ return (0); /* FUNÇÃO ANALISA ARQUIVO DAS SEQÜÊNCIAs. */ int analisa_arq_seq(long int vseq[4]) int i,j,k; long int count=0, soma=0, max=2147483647, maximo=0,minimo=2147483647, media=0; char string[1024000]; /* Abrindo arquivo das seqüências para leitura. */ seq_in=fopen(Arquivo,"r"); if(seq_in==NULL) perror("Erro abrindo arquivo de seqüências.\n"); exit(0); /* Verificando o cabeçalho de formato do arquivo. */ fgets(string,max,seq_in); if(j=strcmp(string,"ObjectData:\n")) printf("(1) Problemas no formato do arquivo --> (%d).\n",j); exit(1); fgets(string,max,seq_in); if(j=strcmp(string,"DATA: string\n")) printf("(2) Problemas no formato do arquivo --> (%d).\n",j); exit(1); fgets(string,max,seq_in); if(j=strcmp(string,"\n")) printf("(3) Problemas no formato do arquivo --> (%d).\n",j); exit(1); i=0; j=1; /* Obendo estatísticas das seqüências. */ while(j) fgets(string,max,seq_in); if(string[0]!='\n' && string[0]!='\r' && string[0]!=' ' && !feof(seq_in)) count = strlen(string)-1; soma += count; if(count>maximo) maximo=count; if(count<minimo && count!=1) minimo=count; i++; string[0]=' '; else j=0;
219
fprintf(stdout,"Arquivo de seqüências: NS = %d, TS = %d, Nós do HMM = %s.\n",i,maximo,Model); media = (int) soma/i; vseq[0] = i; vseq[1] = maximo+1; vseq[2] = minimo; vseq[3] = media; return(1); /* FUNÇÃO DE LEITURA DA SEQÜÊNCIA. */ int read_seq (char As[TS+1]) int i,j; char string[TS+1]; /* reinicializa o indicador de posição do arquivo. */ rewind(seq_in); if(seq_in==NULL) perror("Erro reinicializando indicador de posição.\n"); exit(0); /* Copiando a seqüência do arquivo para uma matriz de strings. */ for(i=0;i<3;i++) fgets(string,TS+1,seq_in); fgets(As,TS+1,seq_in); j=(int)strlen(As); As[j-1]='\0'; /* Fechado o arquivo da seqüência. */ if(fclose(seq_in)!=0) perror("Erro fechando arquivo da seqüência."); exit(0); return(2); /* FUNÇÃO DE LEITURA DOS PARAMETROS DO MODELO. */ int read_model (float PT[][9], float mPE[][20], float iPE[][20]) char nome_arquivo[20] = "hmm", string[TS+1]; long int i, j, state; /* Abre arquivo de probabilidades de transições de estados para leitura. */ strcat(nome_arquivo, Model); strcat(nome_arquivo, ".transicoes"); pt_in=fopen(nome_arquivo,"r"); if(pt_in==NULL) perror("Erro abrindo arquivo de Probabilidades de Transições do HMM.\n"); exit(0); /* Copiando as probabilidades do arquivo para a matriz de Probilidades de Transiçõe. s*/ /* Lendo as 2 primeiras linhas do arquivo (bloco 0 do HMM) e preenchendo as 2 primeiras linhas da matriz. */ fscanf(pt_in,"%d%f%f%f",&state,&PT[1][4],&PT[0][7],&PT[1][1]); fscanf(pt_in,"%d%f%f%f",&state,&PT[1][5],&PT[0][8],&PT[1][2]); /* Lendo as próximas 3*NN-2 linhas do arquivo (blocos de 1 a NN-1 do HMM) e preenchendo as próximas NN-1 linhas da matriz. */ for(i=1;i<NN;i++) fscanf(pt_in,"%d%f%f%f",&state,&PT[i+1][3],&PT[i][6],&PT[i+1][0]); fscanf(pt_in,"%d%f%f%f",&state,&PT[i+1][4],&PT[i][7],&PT[i+1][1]); fscanf(pt_in,"%d%f%f%f",&state,&PT[i+1][5],&PT[i][8],&PT[i+1][2]); /* Lendo as 2 últimas linhas do arquivo e preenchendo as 2 últimas linhas da matriz. */ fscanf(pt_in,"%d%f%f",&state,&PT[NN+1][3],&PT[NN][6]); fscanf(pt_in,"%d%f%f",&state,&PT[NN+1][4],&PT[NN][7]); fscanf(pt_in,"%d%f%f",&state,&PT[NN+1][5],&PT[NN][8]); /* Fechado o arquivo de probabilidades de transições. */ if(fclose(pt_in)!=0) perror("Erro fechando arquivo de Probilidades de Transições."); exit(0); /* Abre arquivo de probabilidades de emissões de símbolos para leitura. */ strcpy(nome_arquivo,"hmm\0"); strcat(nome_arquivo, Model); strcat(nome_arquivo, ".emissoes"); pe_in=fopen(nome_arquivo,"r"); if(pe_in==NULL) perror("Erro abrindo arquivo de Probabiledades de Emissões do HMM.\n"); exit(0); /* Copiando as probabilidades do arquivo para a matriz de Probilidades de Emissões. */ for(i=0;i<NN;i++) fscanf(pe_in,"%d",&state); for(j=0;j<20;j++) fscanf(pe_in,"%f",&iPE[i][j]); fscanf(pe_in,"%d",&state); for(j=0;j<20;j++) fscanf(pe_in,"%f",&mPE[i][j]); fscanf(pe_in,"%d",&state); for(j=0;j<20;j++) fscanf(pe_in,"%f",&iPE[NN][j]); /* Fechado o arquivo de Probilidades de Emissões. */ if(fclose(pe_in)!=0) perror("Erro fechando arquivo de Probilidades de Emissões."); exit(0);
220
return(3); /* FUNÇÃO ALINHA SEQÜÊNCIAS --> VITERBI 2-NÍVEIS DE CHECKPOINTS POR LINHAS */ int viterbi_L2_ckpt_line(float PT[][9],float mPE[][20],float iPE[][20],char As[TS+1]) long int s, t, tt, b, ckpt, inc, li, aTS; char nome_arquivo[20] = "hmm", ch[5]; float logodds, escore, max, aux1, aux2; int i, k, j, dec, ccko, tcbk=0, teste=0, origem=0, CP=0; /* Variáveis usadas para a medição de tempo. */ float wtimes=0.0, ctimes=0.0, wbegins=0.0, cbegins=0.0; /* Obtém o tamanho da seqüência e determina a quantidade de linhas de checkpoints necessárias. */ aTS=strlen(As); CP=(int)sqrt((double)2*NN); if((CP*(CP+1)/2)<NN) CP++; long int ackpt[CP+1]; /* Array para salvar as posições dos checkpoints na matriz_PD. */ /* Array de Checkpoints da Matriz de Viterbi. */ struct matriz_PD float del, mat, ins; /* Guarda os escores dos estados delete, insert e match. */ int tb[3]; /* Guarda o estado anterior no caminho Viterbi: delete=0, insert=1 e match=2.*/ align[CP+1][aTS+1]; /* Uma linha extra para armazenar a linha zero da Matriz_PD. */ /* Inicializa rotina de tempo. */ hpctim(&wtimes,&ctimes,&wbegins,&cbegins); hpctim(&wtimes,&ctimes,&wbegins,&cbegins); /* Inicializa a matriz de checkpoints linha para armazenar os escores de Viterbi. */ for(b=0;b<CP+1;b++) ackpt[b]=INFINITO; for(t=0;t<aTS+1;t++) align[b][t].del = 0.0; align[b][t].mat = 0.0; align[b][t].ins = 0.0; align[b][t].tb[0] = 11; align[b][t].tb[1] = 11; align[b][t].tb[2] = 11; /* Abre arquivo para escrita do escore e do alinhamento. */ strcat(nome_arquivo, Model); strcat(nome_arquivo, ".uclalign"); align_out = fopen(nome_arquivo,"w"); if(align_out == NULL) perror("Erro abrindo arquivo de Alinhamentos do HMM.\n"); exit(0); /* Efetua o alinhamento da seqüência ao modelo e escreve este para um arquivo. */ fprintf(align_out,"Aloritmo de viterbi com checkpoints por linhas (padrão).\n"); fprintf(align_out,"Alinhamento da seqüências com %d radicais a um HMM com %s nós.\n",aTS,Model); /* Efetua o calculo da memória minima requerida. */ aux1=(float) 18*(CP+1)*aTS; aux2=aux1/1024; max=aux2/1024; fprintf(align_out,"A memória mínima alocada é de %f bytes (= %f Kb = %f Mb).\n",aux1,aux2,max); printf("A memória mínima alocada é de %f bytes (= %f Kb = %f Mb).\n",aux1,aux2,max); /* Efetua a tomada de tempo no inicio do alinhameto da seqüência. */ hpctim(&wtimes, &ctimes, &wbegins, &cbegins); /* Enche a linha 0 da matriz de Viterbi para a seqüência s (somente insert). */ /* Primeiro Enche as celulas 0 e 1. */ align[CP][0].del=(float)-INFINITO; align[CP][0].tb[0]=10; align[CP][0].mat=0.0; align[CP][0].tb[1]=9; align[CP][0].ins=(float)-INFINITO; align[CP][0].tb[2]=10; align[CP][1].del=(float)-INFINITO; align[CP][1].tb[0]=10; align[CP][1].mat=(float)-INFINITO; align[CP][1].tb[1]=10; align[CP][1].ins=iPE[0][IAA(As[0])]-f_aa[IAA(As[0])]+PT[0][PMI]; align[CP][1].tb[2]=9; /* Depois as celulas de t=2 até aTS da linha 0. */ for(t=2;t<aTS+1;t++) align[CP][t].del=(float)-INFINITO; align[CP][t].tb[0]=10; align[CP][t].mat =(float)-INFINITO; align[CP][t].tb[1]=10; align[CP][t].ins=iPE[0][IAA(As[t-1])]-f_aa[IAA(As[t-1])]+align[CP][t-1].ins+PT[0][PII]; align[CP][t].tb[2]=PII; /* Enche as demais linhas da matriz de Viterbi para a seqüência s. */ ackpt[CP]=0; /* Linha 0 armazenada no checkpoint CP+1. */ b=1; /* Numeração verdadeira das linhas da Matriz de Viterbi. */
221
ckpt=CP; inc=1; /* Delimitam o tamanho dos ciclos conforme a memória de trabalho disponível. */ dec=0; /* Auxilia no acesso ao último checkpoint salvo a cada ciclo. */ while(ckpt<=NN) for(b=inc;b<=ckpt;b++) if((b-inc)==0) /* Preenche a celula 0 da linha b (somente delete). */ if(b==1) align[b-inc][0].del=align[CP-dec][0].mat+PT[b][PMD]; align[b-inc][0].tb[0]=9; else align[b-inc][0].del=align[CP-dec][0].del+PT[b][PDD]; align[b-inc][0].tb[0]=PDD; align[b-inc][0].mat=(float)-INFINITO; align[b-inc][0].tb[1]=10; align[b-inc][0].ins=(float)-INFINITO; align[b-inc][0].tb[2]=10; /* Preenche as demais celulas da linha b. */ for(t=1;t<aTS+1;t++) /* Preenche o campo delete da celula t da linha b. */ max=align[CP-dec][t].del+PT[b][PDD]; align[b-inc][t].tb[0]=PDD; aux1=align[CP-dec][t].mat+PT[b][PMD]; aux2=align[CP-dec][t].ins+PT[b][PID]; if(max<=aux1) max=aux1; align[b-inc][t].tb[0]=PMD; if(max<aux2) max=aux2; align[b-inc][t].tb[0]=PID; align[b-inc][t].del=max; /* Preenche o campo match da celula t da linha b. */ max=align[CP-dec][t-1].del+PT[b][PDM]; align[b-inc][t].tb[1]=PDM; aux1=align[CP-dec][t-1].mat+PT[b][PMM]; aux2=align[CP-dec][t-1].ins+PT[b][PIM]; if(max<=aux1) max=aux1; if(b==1) align[b-inc][t].tb[1]=9; else align[b-inc][t].tb[1]=PMM; if(max<aux2) max=aux2; align[b-inc][t].tb[1]=PIM; align[b-inc][t].mat=mPE[b-1][IAA(As[t-1])]-f_aa[IAA(As[t-1])]+max; /* Preenche o campo insert da celula t da linha b. */ max=align[b-inc][t-1].del+PT[b][PDI]; align[b-inc][t].tb[2]=PDI; aux1=align[b-inc][t-1].mat+PT[b][PMI]; aux2=align[b-inc][t-1].ins+PT[b][PII]; if(max<=aux1) max=aux1; align[b-inc][t].tb[2]=PMI; if(max<aux2) max=aux2; align[b-inc][t].tb[2]=PII; align[b-inc][t].ins=iPE[b][IAA(As[t-1])]-f_aa[IAA(As[t-1])]+max; if((b-inc)>0) /* Preenche a celula 0 da linha b (somente delete). */ align[b-inc][0].del=align[b-inc-1][0].del+PT[b][PDD]; align[b-inc][0].tb[0]=PDD; align[b-inc][0].mat=(float)-INFINITO; align[b-inc][0].tb[1]=10; align[b-inc][0].ins=(float)-INFINITO; align[b-inc][0].tb[2]=10; /* Preenche as demais celulas da linha b. */ for(t=1;t<aTS+1;t++) /* Preenche o campo delete da celula t da linha b. */ max=align[b-inc-1][t].del+PT[b][PDD]; align[b-inc][t].tb[0]=PDD; aux1=align[b-inc-1][t].mat+PT[b][PMD]; aux2=align[b-inc-1][t].ins+PT[b][PID]; if(max<=aux1) max=aux1; align[b-inc][t].tb[0]=PMD; if(max<aux2) max=aux2; align[b-inc][t].tb[0]=PID; align[b-inc][t].del=max; /* Preenche o campo match da celula t da linha b. */ max=align[b-inc-1][t-1].del+PT[b][PDM]; align[b-inc][t].tb[1]=PDM; aux1=align[b-inc-1][t-1].mat+PT[b][PMM]; aux2=align[b-inc-1][t-1].ins+PT[b][PIM]; if(max<=aux1) max=aux1; align[b-inc][t].tb[1]=PMM; if(max<aux2) max=aux2; align[b-inc][t].tb[1]=PIM; align[b-inc][t].mat=mPE[b-1][IAA(As[t-1])]-f_aa[IAA(As[t-1])]+max; /* Preenche o campo insert da celula t da linha b. */ max=align[b-inc][t-1].del+PT[b][PDI]; align[b-inc][t].tb[2]=PDI; aux1=align[b-inc][t-1].mat+PT[b][PMI]; aux2=align[b-inc][t-1].ins+PT[b][PII]; if(max<=aux1) max=aux1; align[b-inc][t].tb[2]=PMI; if(max<aux2) max=aux2; align[b-inc][t].tb[2]=PII; align[b-inc][t].ins=iPE[b][IAA(As[t-1])]-f_aa[IAA(As[t-1])]+max; dec++;
222
ackpt[b-1-inc]=ckpt; /* Linha ckpt-1 (ou b-1) armazenada no checkpoint b-1-inc. */ if(ckpt==NN) ckpt++; else if((ckpt+CP-dec)<=NN) inc=ckpt+1; ckpt=ckpt+CP-dec; else inc=ckpt+1; ckpt=NN; ckpt--; ccko=dec; if(ackpt[CP-dec]!=INFINITO) k=CP-dec+1; else k=NN-inc+1; dec=CP-k+1; /* Conclui a fase forward --> Preenche o campo match da celula t da linha b. */ max=align[CP-dec][t-1].del+PT[b][PDM]; origem=PDM; aux1=align[CP-dec][t-1].mat+PT[b][PMM]; aux2=align[CP-dec][t-1].ins+PT[b][PIM]; if(max<=aux1) max=aux1; origem=PMM; if(max<aux2) max=aux2; origem=PIM; logodds=max; escore=exp2(logodds); fprintf(align_out,"Escore Viterbi: seqüência com %d radicais -> %f (log-odds = %f).\n",aTS,escore,logodds); /* Fase de retrocedimento sobre a matriz de Viterbi para recuperar o melhor caminho * Usar a função scanf() para imprimir os símbolos numa matriz (string) de char e * depois fazer as manipulações necessárias para imprimir corretamente num arquivo. */ b=NN+1;li=0; tt=aTS+1; tcbk=origem; while(ccko>=1) while(k>0) teste=tcbk; if(teste==PDD || teste==PMD || teste==PID) b--; k--; if(teste==PDD) fprintf(align_out,"D%d(-) ",b); tcbk=align[k][tt].tb[0]; else if(teste==PMD) fprintf(align_out,"M%d(%c) ",b,As[tt-1]); tcbk=align[k][tt].tb[1]; else fprintf(align_out,"I%d(%c) ",b,As[tt-1]); tcbk=align[k][tt].tb[2]; if(teste==PDM || teste==PMM || teste==PIM) b--; tt--; k--; if(teste==PDM) fprintf(align_out,"D%d(-) ",b); tcbk=align[k][tt].tb[0]; else if(teste==PMM) fprintf(align_out,"M%d(%c) ",b,As[tt-1]); tcbk=align[k][tt].tb[1]; else fprintf(align_out,"I%d(%c) ",b,As[tt-1]); tcbk=align[k][tt].tb[2]; if(teste==PDI || teste==PMI || teste==PII) tt--; if(teste==PDI) fprintf(align_out,"D%d(-) ",b); tcbk=align[k][tt].tb[0]; else if(teste==PMI) fprintf(align_out,"M%d(%c) ",b,As[tt-1]); tcbk=align[k][tt].tb[1]; else fprintf(align_out,"I%d(%c) ",b,As[tt-1]); tcbk=align[k][tt].tb[2]; while(tcbk==PDI || tcbk== PMI || tcbk==PII) teste=tcbk; if(teste==PDI || teste==PMI || teste==PII) tt--; if(teste==PDI) fprintf(align_out,"D%d(-) ",b); tcbk=align[k][tt].tb[0]; else if(teste==PMI) fprintf(align_out,"M%d(%c) ",b,As[tt-1]); tcbk=align[k][tt].tb[1]; else fprintf(align_out,"I%d(%c) ",b,As[tt-1]); tcbk=align[k][tt].tb[2]; ccko--; if(ccko>=1) k=CP-ccko+1; li=ackpt[k]+1; for(j=li;j<b;j++) if((j-li)==0) /* Preenche a celula 0 da linha b (somente delete). */ if(j==1) align[j-li][0].del=align[k][0].mat+PT[j][PMD]; align[j-li][0].tb[0]=9; else align[j-li][0].del=align[k][0].del+PT[j][PDD]; align[j-li][0].tb[0]=PDD; align[j-li][0].mat=(float)-INFINITO; align[j-li][0].tb[1]=10; align[j-li][0].ins=(float)-INFINITO; align[j-li][0].tb[2]=10; /* Preenche as demais celulas da linha b. */
223
for(t=1;t<aTS+1;t++) /* Pode-se implementar retrocedimento restrito fazendo aTS==tt. */ /* Preenche o campo delete da celula t da linha b. */ max=align[k][t].del+PT[j][PDD]; align[j-li][t].tb[0]=PDD; aux1=align[k][t].mat+PT[j][PMD]; aux2=align[k][t].ins+PT[j][PID]; if(max<=aux1) max=aux1; align[j-li][t].tb[0]=PMD; if(max<aux2) max=aux2; align[j-li][t].tb[0]=PID; align[j-li][t].del=max; /* Preenche o campo match da celula t da linha b. */ max=align[k][t-1].del+PT[j][PDM]; align[j-li][t].tb[1]=PDM; aux1=align[k][t-1].mat+PT[j][PMM]; aux2=align[k][t-1].ins+PT[j][PIM]; if(max<=aux1) max=aux1; if(j==1) align[j-li][t].tb[1]=9; else align[j-li][t].tb[1]=PMM; if(max<aux2) max=aux2; align[j-li][t].tb[1]=PIM; align[j-li][t].mat=mPE[j-1][IAA(As[t-1])]-f_aa[IAA(As[t-1])]+max; /* Preenche o campo insert da celula t da linha b. */ max=align[j-li][t-1].del+PT[j][PDI]; align[j-li][t].tb[2]=PDI; aux1=align[j-li][t-1].mat+PT[j][PMI]; aux2=align[j-li][t-1].ins+PT[j][PII]; if(max<=aux1) max=aux1; align[j-li][t].tb[2]=PMI; if(max<aux2) max=aux2; align[j-li][t].tb[2]=PII; align[j-li][t].ins=iPE[j][IAA(As[t-1])]-f_aa[IAA(As[t-1])]+max; if((j-li)>0) /* Preenche a celula 0 da linha b (somente delete). */ align[j-li][0].del=align[j-li-1][0].del+PT[j][PDD]; align[j-li][0].tb[0]=PDD; align[j-li][0].mat=(float)-INFINITO; align[j-li][0].tb[1]=10; align[j-li][0].ins=(float)-INFINITO; align[j-li][0].tb[2]=10; /* Preenche as demais celulas da linha b. */ for(t=1;t<aTS+1;t++) /* Pode-se implementar retrocedimento restrito fazendo aTS==tt. */ /* Preenche o campo delete da celula t da linha b. */ max=align[j-li-1][t].del+PT[j][PDD]; align[j-li][t].tb[0]=PDD; aux1=align[j-li-1][t].mat+PT[j][PMD]; aux2=align[j-li-1][t].ins+PT[j][PID]; if(max<=aux1) max=aux1; align[j-li][t].tb[0]=PMD; if(max<aux2) max=aux2; align[j-li][t].tb[0]=PID; align[j-li][t].del=max; /* Preenche o campo match da celula t da linha b. */ max=align[j-li-1][t-1].del+PT[j][PDM]; align[j-li][t].tb[1]=PDM; aux1=align[j-li-1][t-1].mat+PT[j][PMM]; aux2=align[j-li-1][t-1].ins+PT[j][PIM]; if(max<=aux1) max=aux1; align[j-li][t].tb[1]=PMM; if(max<aux2) max=aux2; align[j-li][t].tb[1]=PIM; align[j-li][t].mat=mPE[j-1][IAA(As[t-1])]-f_aa[IAA(As[t-1])]+max; /* Preenche o campo insert da celula t da linha b. */ max=align[j-li][t-1].del+PT[j][PDI]; align[j-li][t].tb[2]=PDI; aux1=align[j-li][t-1].mat+PT[j][PMI]; aux2=align[j-li][t-1].ins+PT[j][PII]; if(max<=aux1) max=aux1; align[j-li][t].tb[2]=PMI; if(max<aux2) max=aux2; align[j-li][t].tb[2]=PII; align[j-li][t].ins=iPE[j][IAA(As[t-1])]-f_aa[IAA(As[t-1])]+max; if(tcbk==PIM || tcbk== PID) while(tcbk==PIM || tcbk== PID || tcbk== PII) teste=tcbk; if(teste==PDD || teste==PMD || teste==PID) b--; fprintf(align_out,"I%d(%c) ",b,As[tt-1]); tcbk=align[CP][tt].tb[2]; if(teste==PIM) b--; tt--; fprintf(align_out,"I%d(%c) ",b,As[tt-1]); tcbk=align[CP][tt].tb[2];
224
if(teste==PII) tt--; fprintf(align_out,"I%d(%c) ",b,As[tt-1]); tcbk=align[CP][tt].tb[2]; fprintf(align_out,"\n"); /* Efetua a tomada de tempo no final do alinhameto da seqüência. */ hpctim(&wtimes, &ctimes, &wbegins, &cbegins); printf("WALL time gasto no alinhamento --> %f.\n", wtimes); printf("CPU time gasto no alinhamento --> %f.\n", ctimes); fprintf(align_out,"WALL time gasto no alinhamento da seqüência --> %f.\n",wtimes); fprintf(align_out,"CPU time gasto no alinhamento da seqüência --> %f.\n",ctimes); /* Fechado o arquivo de Alimhamentos */ if(fclose(align_out)!=0) perror("Erro fechando arquivo de escrita do alinhamento."); exit(0); return(4);
225
226
APÊNDICE 3
ALGORITMO DE VITERBI COM 2-NÍVEIS DE CHECKPOINTS POR DIAGONAIS, COM PARTICIONAMENTO MÓVEL DE MEMÓRIA E
RETROCEDIMENTO RESTRITO
/* **************************************************************************************** * Algoritmo de Viterbi para alinhar seqüências de aminoácidos * Copyright (C) 2004 José Olimpio Ferreira * Instituto Nacional de Pesquisas Espaciais e * Universidade Católica de Goias * All Rights Reserved * **************************************************************************************** * * align_viterbi_ckpts_diag_uni.c * * --> Algoritmo de Viterbi para obter o melhor alinhamento de seqüências de aminoácidos a um HMM. * --> Este é um algoritmo de 2-níveis com checkpoints por diagonais com retrocedimento restrito * --> Requer espaço da ordem de O(4*m*SQRT(n)). * **************************************************************************************** * * Entradas: * --> Arquivo de probabilidades de transição de estados --> hmm[nos].transicoes * --> Arquivo de probabilidades de emissão de símbolos --> hmm[nos].emissoes * --> Arquivo de seqüências para alinhamento ao HMM --> <nome_arquivo> * --> nós = tamnho do HMM * * Saida: * --> Escore + Alinhamento Viterbi da seqüência ao HMM --> hmm[nos].dlalign * **************************************************************************************** * * Compilação - Linkedição - Construção do executável -> exemplo para intel pentium 3 celerom * --> gcc -c -march=pentium3 -mcpu=pentium3 -mfpmath=sse -O3 align_viterbi_ckpts_diag_uni.c * --> gcc -c -march=pentium3 -mcpu=pentium3 -mfpmath=sse -O3 hpctim.c * --> gcc -c -march=pentium3 -mcpu=pentium3 -mfpmath=sse -O3 hpcwall.c * --> gcc -c -march=pentium3 -mcpu=pentium3 -mfpmath=sse -O3 etime.c * --> gcc -o align_viterbi_ckpts_diag_uni align_viterbi_ckpts_diag_uni.o hpctim.o hpcwall.o etime.o -lm * * Compilação - Linkedição - Construção do executável -> exemplo para amd athlon k-7 * --> gcc -c -march=athlon -mcpu=athlon -mfpmath=387 -O3 align_viterbi_ckpts_diag_uni.c * --> gcc -c -march=athlon -mcpu=athlon -mfpmath=387 -O3 hpctim.c * --> gcc -c -march=athlon -mcpu=athlon -mfpmath=387 -O3 hpcwall.c * --> gcc -c -march=athlon -mcpu=athlon -mfpmath=387 -O3 etime.c * --> gcc -o align_viterbi_ckpts_diag_uni align_viterbi_ckpts_diag_uni.o hpctim.o hpcwall.o etime.o -lm * **************************************************************************************** */ #include <stdio.h> #include <stdlib.h> #include <math.h> #include <time.h> #include <string.h> /*definie algumas macros */ #define IAA(x) (strchr(aa, (x)) - aa) /* Índice de símbolos de aminoácidos */ #define log2(x) ((x)>0.0 ? log(x)*1.44269504 : -19) /* logaritmo de base 2 */ #define exp2(x) (exp(x)*0.69314718) /* exponencial de base 2 */ #define INFINITO 99999999 /* infinito para log-probilidades de valores zero */ #define aa_alfabeto "ACDEFGHIKLMNPQRSTVWY" /* Alfabeto não-degenerado de aminoácidos */
227
/* String contendo os símbolos não-degenerados dos aminoácidos */ const char aa[21] = aa_alfabeto; /* Índices para as probabilidades de transição de estado do HMM */ enum transicoes_indices PDD=0,PMD=1,PID=2,PDM=3,PMM=4,PIM=5,PDI=6,PMI=7,PII=8; /* Variáveis: * NS --> número de seqüências * TS --> tamanho da maior seqüência * NN --> número de nós do HMM * Model --> HMM[500,1000,2000,4000,8000] nós * Arquivo --> nome do arquivo que contém as seqüências no formato HMMpro */ const long int NS, TS, NN; char Model[80], Arquivo[80]; /* --> Freqüências f(i) - Dayhoff - de occorrência dos 20 aminoácidos (função massa de probabilidade * para aminoácidos) convertidas em log-probabilidades. * --> Obtidos do BD SwissProt 34: considerando 21.210.388 radicais. * --> Apresentadas em ordem alfabética por código de letras simples. * --> Usadas como probabilidades de emissão de símbolos no modelo nulo ou aleatório (R). */ const float f_aa[20] = (float)-3.72699742, /* (float)0.075520, /* A */ (float)-5.88061460, /* (float)0.016973, /* C */ (float)-4.23707464, /* (float)0.053029, /* D */ (float)-3.98384032, /* (float)0.063204, /* E */ (float)-4.61663135, /* (float)0.040762, /* F */ (float)-3.86884780, /* (float)0.068448, /* G */ (float)-5.47997107, /* (float)0.022406, /* H */ (float)-4.12572395, /* (float)0.057284, /* I */ (float)-4.07344183, /* (float)0.059398, /* K */ (float)-3.42044908, /* (float)0.093399, /* L */ (float)-5.40696564, /* (float)0.023569, /* M */ (float)-4.46456809, /* (float)0.045293, /* N */ (float)-4.34338099, /* (float)0.049262, /* P */ (float)-4.63554859, /* (float)0.040231, /* Q */ (float)-4.27724022, /* (float)0.051573, /* R */ (float)-3.79157763, /* (float)0.072214, /* S */ (float)-4.12144885, /* (float)0.057454, /* T */ (float)-3.93783407, /* (float)0.065252, /* V */ (float)-6.32042847, /* (float)0.012513, /* W */ (float)-4.96646070 /* (float)0.031985 /* Y */ ; /* Ponteiros para arquivos */ FILE *seq_in, *pt_in, *pe_in, *align_out; /* Declaração dos protótipos das funções internas .*/ int analisa_arq_seq(long int vseq[4]); int read_seq (char As[TS+1]); int read_model (float PT[][9], float mPE[][20], float iPE[][20]); int viterbi_L2_ckpt_diag(float PT[][9],float mPE[][20],float iPE[][20],char As[TS+1]); /* Declaração dos protótipos das funções externas */ void etime(float *result); void hpcwall(double *retval); void hpctim(float *wtime, float *ctime, float *wbegin, float *cbegin); /* FUNÇÃO PRINCIPAL --> MAIN */ int main (int argc, char *argv[]) long int i, j, vseq[4]=0,0,0,0, *ptr; int status; /* Trata os argumentos da função principal. */ if(argc!=3) printf("\nVocê esqueceu de informar:\n"); printf("o tamanho do HMM (Opções: 500, 1000, 2000, 4000 e 8000),\n"); printf("o nome do arquivo contendo as seqüências.\n\n"); printf("Entre: <nome_programa> <tamanho_HMM> <nome_arquivo>\n"); printf("O arquivo deve estar no formato HMMpro --> http://www.netid.com\n"); exit(1); /* Inicializa algumas variáveis globais do tipo char* e const int. Usadas como informações de nomes de arquivos, de tamnho do modelo e de dimensões de matrizes. */ strcpy(Model,argv[1]); (const) ptr = &NN; *ptr = atoi(argv[1]);
228
strcpy(Arquivo,argv[2]); /* Chamada para a função análise das seqüências. */ status = analisa_arq_seq(vseq); /* Verifica o status do retorno da função analisa_arq_seq. */ if(status!=1) printf("Erro na análise das seqüêcias.\n"); return(0); (const) ptr = &NS; *ptr = vseq[0]; (const) ptr = &TS; *ptr = vseq[1]; /* Matriz usada para armazenar a seqüência a ser alinhada. */ char strAs[TS+1]; /* Chamada para a função de leitura da seqüência. */ status = read_seq(strAs); /* Verifica o status do retorno da função read_seq. */ if(status!=2) printf("Erro na leitura da seqüêcia.\n"); return(0); /* Matriz de probabilidades de transição do HMM. */ float pt_hmm[NN+2][9]; for(i=0;i<NN+2;i++) for(j=0;j<9;j++) pt_hmm[i][j] = (float)0.0; /* Matriz de probabilidades de emissão de símbolos dos estados MATCH. */ float mf_aa[NN][20]; for(i=0;i<NN;i++) for(j=0;j<20;j++) mf_aa[i][j] = 0.0; /* Matriz de probabilidades de emissão de símbolos dos estados INSERT. */ float if_aa[NN+1][20]; for(i=0;i<NN+1;i++) for(j=0;j<20;j++) if_aa[i][j] = 0.0; /* Chamada para a função de leitura de parametros do HMM. */ status = read_model(pt_hmm, mf_aa, if_aa); /* Verifica o status do retorno da função read_model. */ if(status!=3) printf("Erro na leitura dos parametros do HMM\n"); return(0); /* convert probabilidades para log-probabilidades. */ for(i=0;i<NN+2;i++) for(j=0;j<9;j++) pt_hmm[i][j] = log2(pt_hmm[i][j]); for(i=0;i<NN;i++) for(j=0;j<20;j++) if_aa[i][j] = log2(if_aa[i][j]); for(j=0;j<20;j++) mf_aa[i][j] = log2(mf_aa[i][j]); for(j=0;j<20;j++) if_aa[i][j] = log2(if_aa[i][j]); /* Chamada para a função de alinhamento da seqüência ao HMM. */ status = viterbi_L2_ckpt_diag(pt_hmm, mf_aa, if_aa, strAs); /* Verifica o status do retorno da função viterbi_L2_ckpt_diag. */ if(status!=4) printf("Erro no alinhamento da seqüência ao HMM\n"); return(0); return (0); /* FUNÇÃO ANALISA ARQUIVO DE SEQÜÊNCIAS. */ int analisa_arq_seq(long int vseq[4]) int i,j; long int count=0, soma=0, max=2048000, maximo=0,minimo=2048000, media=0; char string[2048000]; /* Abindo arquivo de seqüências para leitura. */ seq_in=fopen(Arquivo,"r"); if(seq_in==NULL) perror("Erro abrindo arquivo de seqüências.\n"); exit(0); /* Verificando o cabeçalho de formato do arquiv. */ fgets(string,max,seq_in); if(j=strcmp(string,"ObjectData:\n")) printf("(1) Problemas no formato do arquivo --> (%d).\n",j); exit(1); fgets(string,max,seq_in); if(j=strcmp(string,"DATA: string\n")) printf("(2) Problemas no formato do arquivo --> (%d).\n",j); exit(1); fgets(string,max,seq_in); if(j=strcmp(string,"\n")) printf("(3) Problemas no formato do arquivo --> (%d).\n",j); exit(1); i=0; j=1; /* Obendo estatísticas das seqüências. */ while(j) fgets(string,max,seq_in); if(string[0]!='\n' && string[0]!='\r' && string[0]!=' ' && !feof(seq_in)) count = strlen(string)-1; soma += count; if(count>maximo) maximo=count; if(count<minimo && count!=1) minimo=count; i++; string[0]=' '; else j=0;
229
fprintf(stdout,"NS = %d, TS = %d, Nós do HMM = %s.\n",i,maximo,Model); media = (int) soma/i; vseq[0] = i; vseq[1] = maximo+1; vseq[2] = minimo; vseq[3] = media; return(1); /* FUNÇÃO DE LEITURA DA SEQÜÊNCIA. */ int read_seq (char As[TS+1]) int i; char string[TS+1]; /* reinicializa o indicador de posição do arquivo. */ rewind(seq_in); if(seq_in==NULL) perror("Erro reinicializando indicador de posição.\n"); exit(0); /* Copiando a seqüência do arquivo para uma matriz de strings. */ for(i=0;i<3;i++) fgets(string,TS+1,seq_in); fgets(As,TS+1,seq_in); i=(int)strlen(As); As[i-1]='\0'; /* Fechado o arquivo da seqüência. */ if(fclose(seq_in)!=0) perror("Erro fechando arquivo de seqüências."); exit(0); return(2); /* FUNÇÃO DE LEITURA DOS PARÂMETROS DO MODELO */ int read_model (float PT[][9], float mPE[][20], float iPE[][20]) char nome_arquivo[20] = "hmm", string[TS+1]; long int i, j, state; /* Abre arquivo de probabilidades de transições de estados para leitura. */ strcat(nome_arquivo, Model); strcat(nome_arquivo, ".transicoes"); pt_in=fopen(nome_arquivo,"r"); if(pt_in==NULL) perror("Erro abrindo arquivo de Probabiledades de Transições do HMM.\n"); exit(0); /* Copiando as probabilidades do arquivo para a matriz de Probabilidades de Transições. */ /* Lendo as 2 primeiras linhas do arquivo (bloco 0 do HMM) e preenchendo as 2 primeiras linhas da matriz. */ fscanf(pt_in,"%d%f%f%f",&state,&PT[1][4],&PT[0][7],&PT[1][1]); fscanf(pt_in,"%d%f%f%f",&state,&PT[1][5],&PT[0][8],&PT[1][2]); /* Lendo as próximas 3*NN-2 linhas do arquivo (blocos de 1 a NN-1 do HMM) e preenchendo as próximas NN-1 linhas da matriz. */ for(i=1;i<NN;i++) fscanf(pt_in,"%d%f%f%f",&state,&PT[i+1][3],&PT[i][6],&PT[i+1][0]); fscanf(pt_in,"%d%f%f%f",&state,&PT[i+1][4],&PT[i][7],&PT[i+1][1]); fscanf(pt_in,"%d%f%f%f",&state,&PT[i+1][5],&PT[i][8],&PT[i+1][2]); /* Lendo as 2 últimas linhas do arquivo e preenchendo as 2 últimas linhas da matriz. */ fscanf(pt_in,"%d%f%f",&state,&PT[NN+1][3],&PT[NN][6]); fscanf(pt_in,"%d%f%f",&state,&PT[NN+1][4],&PT[NN][7]); fscanf(pt_in,"%d%f%f",&state,&PT[NN+1][5],&PT[NN][8]); /* Fechado o arquivo de de Probabilidades de Transições. */ if(fclose(pt_in)!=0) perror("Erro fechando arquivo de Probabilidades Transições."); exit(0); /* Abre arquivo de probabilidades de emissões de símbolos para leitura. */ strcpy(nome_arquivo,"hmm\0"); strcat(nome_arquivo, Model); strcat(nome_arquivo, ".emissoes"); pe_in=fopen(nome_arquivo,"r"); if(pe_in==NULL) perror("Erro abrindo arquivo de Probabiledades de Emissões do HMM.\n"); exit(0); /* Copiando as probabilidades do arquivo para a matriz de Probabilidades de Emissões. */ for(i=0;i<NN;i++) fscanf(pe_in,"%d",&state); for(j=0;j<20;j++) fscanf(pe_in,"%f",&iPE[i][j]); fscanf(pe_in,"%d",&state); for(j=0;j<20;j++) fscanf(pe_in,"%f",&mPE[i][j]); fscanf(pe_in,"%d",&state); for(j=0;j<20;j++) fscanf(pe_in,"%f",&iPE[NN][j]); /* Fechado o arquivo de Probabilidades de Emissões. */
230
if(fclose(pe_in)!=0) perror("Erro fechando arquivo de Probabilidade Emissões."); exit(0); return(3); /* FUNÇÃO ALINHA SEQÜÊNCIA --> VITERBI 2-NÍVEIS DE CHECKPOINTS POR DIAGONAIS */ int viterbi_L2_ckpt_diag(float PT[][9],float mPE[][20],float iPE[][20],char As[TS+1]) long int t, tt, b, aTS, ckpt, inc, l, li, low, lower, upper, d; char nome_arquivo[20] = "hmm", ch[5]; float logodds, escore, max, aux1, aux2; int i, k, j, dec, ccko, tcbk=0, teste=0, origem=0, CP=0, flag=0; /* Variáveis usadas para a medição de tempo. */ float wtimes=0.0, ctimes=0.0, wbegins=0.0, cbegins=0.0; /* Obtém o tamanho da seqüência e determina o número de diagonais da matriz de viterbi. */ aTS=strlen(As); d=NN+aTS+1; /* Determina a quantidade de diagonais de checkpoints necessárias. */ CP=(int)sqrt((double)(4*d))); /* O(2*sqrt(d)) <=> O(2*sqrt(2n)) <= O(4*sqrt(n)) */ if(CP%2>0) CP++; while((0.5*CP*(0.5*CP+1))<d) CP=CP+2; CP=CP+2; /* 2 diagonais extras para armazenar as diagonais 0 e 1. */ long int ackpt[CP]; /* Array para salvar as posições dos checkpoints na matriz_PD. */ /* Array de Checkpoints da Matriz de Viterbi (matriz_PD). */ struct matriz_PD float del, mat, ins; /* Guarda os escores para os estados delete, insert e match. */ int tb[3]; /* Guarda o estado anterior no caminho Viterbi: delete=0, match=1 e insert=2. */ diag[CP][aTS+1]; /* Inicializa rotina de tempo. */ hpctim(&wtimes,&ctimes,&wbegins,&cbegins); hpctim(&wtimes,&ctimes,&wbegins,&cbegins); /* Inicializa o Array de Checkpoints da matriz de escores de Viterbi. */ for(b=0;b<CP;b++) ackpt[b]=INFINITO; for(t=0;t<aTS+1;t++) diag[b][t].del = 0.0; diag[b][t].mat = 0.0; diag[b][t].ins = 0.0; /* poderia ser + INFINITO */ diag[b][t].tb[0] = 11; diag[b][t].tb[1] = 11; diag[b][t].tb[2] = 11; /* Abre arquivo de saída para escrita do alinhamento */ strcat(nome_arquivo, Model); strcat(nome_arquivo, ".udalign"); align_out = fopen(nome_arquivo,"w"); if(align_out == NULL) perror("Erro abrindo arquivo de do alinhamento ao HMM.\n"); exit(0); /* Efetua o alinhamento da seqüência ao modelo e escreve este para o arquivo. */ fprintf(align_out,"Aloritmo de viterbi de 2-níveis de checkpoints por diagonais e retrocedimento restrito.\n"); fprintf(align_out,"Alinhamento da seqüência com %d radicais a um HMM com %s nós\n",aTS,Model); // fprintf(align_out,"Foram usados %d diagonais de checkpoints.\n",CP); // fprintf(align_out,"Seqüência --> %s\n",As); /* Efetua o cálculo da memória mínima requerida. */ aux1=(float) 18*(CP)*(aTS+1); aux2=aux1/1024; max=aux2/1024; fprintf(align_out,"A memória mínima alocada é de %f bytes (= %f Kbytes = %f Mbytes)\n",aux1,aux2,max); printf("A memória mínima alocada é de %f bytes (= %f Kbytes = %f Mbytes)\n",aux1,aux2,max); /* Efetua a tomada de tempo no início do alinhameto da seqüência. */ hpctim(&wtimes, &ctimes, &wbegins, &cbegins); /* Enche a diagonal 0 da matriz de Viterbi para a seqüência (somente insert). */ diag[CP-2][0].del=(float)-INFINITO; diag[CP-2][0].tb[0]=10; diag[CP-2][0].mat=0.0; diag[CP-2][0].tb[1]=9; diag[CP-2][0].ins=(float)-INFINITO; diag[CP-2][0].tb[2]=10; ackpt[CP-2]=0; /* diagonal 0 armazenada no checkpoint CP-2. */ /* Enche a diagonal 1 da matriz de Viterbi para a seqüência. */ diag[CP-1][0].del=diag[CP-1][0].mat+PT[1][PMD]; diag[CP-1][0].tb[0]=9; diag[CP-1][0].mat=(float)-INFINITO; diag[CP-1][0].tb[1]=10; diag[CP-1][0].ins=(float)-INFINITO; diag[CP-1][0].tb[2]=10; diag[CP-1][1].del=(float)-INFINITO; diag[CP-1][1].tb[0]=10;
231
diag[CP-1][1].mat=(float)-INFINITO; diag[CP-1][1].tb[1]=10; diag[CP-1][1].ins=iPE[0][IAA(As[0])]-f_aa[IAA(As[0])]+PT[0][PMI]; diag[CP-1][1].tb[2]=9; ackpt[CP-1]=1; /* diagonal 1 armazenada no checkpoint CP-1. */ /* Enche as demais diagonais da matriz de Viterbi para a seqüência. */ b=2; /* Numeração verdadeira das diagonais da Matriz de Viterbi. */ ckpt=CP-1; inc=2; /* Delimitam o tamanho dos ciclos conforme a memória de trabalho disponível. */ dec=0; /* Auxilia no acesso aos dois último checkpoints salvos a cada ciclo. */ while(ckpt<d) for(b=inc;b<=ckpt;b++) /* Caso o HMM seja maior do que a seqüência (NN > aTS). */ if(NN>aTS) if(b<=aTS) lower=0; upper=b; if(b>aTS && b<=NN) lower=0; upper=aTS; if(b>NN) lower=b-NN; upper=aTS; /* Caso o HMM seja igual a seqüência (NN = aTS). */ if(NN==aTS) if(b<=NN) lower=0; upper=b; else lower=b-NN; upper=aTS; /* Caso o HMM seja menor do que a seqüência (NN < aTS). */ if(NN<aTS) if(b<=NN) lower=0; upper=b; if(b<=aTS && b>NN) lower=b-NN; upper=b; if(b>aTS) lower=b-NN; upper=aTS; low=lower; if((b-inc)==0) /* A diagonal b-inc (checkpoint 0) depende dos dois últimos checkpoints armazenados. */ if(lower==0) /* Preenche a célula 0 (linha b) da diagonal b (somente delete). */ diag[b-inc][0].del=diag[CP-dec-1][0].del+PT[b][PDD]; diag[b-inc][0].tb[0]=PDD; diag[b-inc][0].mat=(float)-INFINITO; diag[b-inc][0].tb[1]=10; diag[b-inc][0].ins=(float)-INFINITO; diag[b-inc][0].tb[2]=10; lower++; /* Preenche as demais células da diagonal b. */ for(t=lower;t<=upper;t++) if((b-t)>0) /* Preenche as células correspondentes às linhas b-1 até 1 na diagonal b. */ /* Preenche o campo delete da célula t da diagonal b. */ max=diag[CP-dec-1][t].del+PT[b-t][PDD]; diag[b-inc][t].tb[0]=PDD; aux1=diag[CP-dec-1][t].mat+PT[b-t][PMD]; aux2=diag[CP-dec-1][t].ins+PT[b-t][PID]; if(max<=aux1) max=aux1; diag[b-inc][t].tb[0]=PMD; if(max<aux2) max=aux2; diag[b-inc][t].tb[0]=PID; diag[b-inc][t].del=max; /* Preenche o campo match da célula t da diagonal b. */ max=diag[CP-dec-2][t-1].del+PT[b-t][PDM]; diag[b-inc][t].tb[1]=PDM; aux1=diag[CP-dec-2][t-1].mat+PT[b-t][PMM]; aux2=diag[CP-dec-2][t-1].ins+PT[b-t][PIM]; if(max<=aux1) max=aux1; if(b==2 && (b-t)==1) diag[b-inc][t].tb[1]=9; else diag[b-inc][t].tb[1]=PMM; if(max<aux2) max=aux2; diag[b-inc][t].tb[1]=PIM; diag[b-inc][t].mat=mPE[b-t-1][IAA(As[t-1])]-f_aa[IAA(As[t-1])]+max; /* Preenche o campo insert da célula t da diagonal b. */ max=diag[CP-dec-1][t-1].del+PT[b-t][PDI]; diag[b-inc][t].tb[2]=PDI; aux1=diag[CP-dec-1][t-1].mat+PT[b-t][PMI]; aux2=diag[CP-dec-1][t-1].ins+PT[b-t][PII]; if(max<=aux1) max=aux1; diag[b-inc][t].tb[2]=PMI; if(max<aux2) max=aux2; diag[b-inc][t].tb[2]=PII; diag[b-inc][t].ins=iPE[b-t][IAA(As[t-1])]-f_aa[IAA(As[t-1])]+max; /* Se b-t == 0 a célula pertence a linha 0 e recebe tratamento especial (somente insert). */ else /* Preenche a célula correspondentes à linha 0 na diagonal b. */ diag[b-inc][t].del=(float)-INFINITO; diag[b-inc][t].tb[0]=10; diag[b-inc][t].mat=(float)-INFINITO; diag[b-inc][t].tb[1]=10;
232
diag[b-inc][t].ins=iPE[0][IAA(As[t-1])]-f_aa[IAA(As[t-1])]+diag[CP-dec-1][t-1].ins+PT[0][PII]; diag[b-inc][t].tb[2]=PII; if((b-inc)==1) /* A diagonal b-inc (checkpoint 0) depende do último checkpoint armazenado. */ if(lower==0) /* Preenche a célula 0 (linha b) da diagonal b (somente delete). */ diag[b-inc][0].del=diag[b-inc-1][0].del+PT[b][PDD]; diag[b-inc][0].tb[0]=PDD; diag[b-inc][0].mat=(float)-INFINITO; diag[b-inc][0].tb[1]=10; diag[b-inc][0].ins=(float)-INFINITO; diag[b-inc][0].tb[2]=10; lower++; /* Preenche as demais células da diagonal b. */ for(t=lower;t<=upper;t++) if((b-t)>0) /* Preenche as células correspondentes às linhas b-1 até 1 na diagonal b. */ /* Preenche o campo delete da célula t da diagonal b. */ max=diag[b-inc-1][t].del+PT[b-t][PDD]; diag[b-inc][t].tb[0]=PDD; aux1=diag[b-inc-1][t].mat+PT[b-t][PMD]; aux2=diag[b-inc-1][t].ins+PT[b-t][PID]; if(max<=aux1) max=aux1; diag[b-inc][t].tb[0]=PMD; if(max<aux2) max=aux2; diag[b-inc][t].tb[0]=PID; diag[b-inc][t].del=max; /* Preenche o campo match da célula t da diagonal b. */ max=diag[CP-dec-1][t-1].del+PT[b-t][PDM]; diag[b-inc][t].tb[1]=PDM; aux1=diag[CP-dec-1][t-1].mat+PT[b-t][PMM]; aux2=diag[CP-dec-1][t-1].ins+PT[b-t][PIM]; if(max<=aux1) max=aux1; diag[b-inc][t].tb[1]=PMM; if(max<aux2) max=aux2; diag[b-inc][t].tb[1]=PIM; diag[b-inc][t].mat=mPE[b-t-1][IAA(As[t-1])]-f_aa[IAA(As[t-1])]+max; /* Preenche o campo insert da célula t da diagonal b. */ max=diag[b-inc-1][t-1].del+PT[b-t][PDI]; diag[b-inc][t].tb[2]=PDI; aux1=diag[b-inc-1][t-1].mat+PT[b-t][PMI]; aux2=diag[b-inc-1][t-1].ins+PT[b-t][PII]; if(max<=aux1) max=aux1; diag[b-inc][t].tb[2]=PMI; if(max<aux2) max=aux2; diag[b-inc][t].tb[2]=PII; diag[b-inc][t].ins=iPE[b-t][IAA(As[t-1])]-f_aa[IAA(As[t-1])]+max; /* Se b-t == 0 a célula pertence a linha 0 e recebe tratamento especial (somente insert). */ else /* Preenche a célula correspondentes à linha 0 na diagonal b. */ diag[b-inc][t].del=(float)-INFINITO; diag[b-inc][t].tb[0]=10; diag[b-inc][t].mat=(float)-INFINITO; diag[b-inc][t].tb[1]=10; diag[b-inc][t].ins=iPE[0][IAA(As[t-1])]-f_aa[IAA(As[t-1])]+diag[b-inc-1][t-1].ins+PT[0][PII]; diag[b-inc][t].tb[2]=PII; if((b-inc)>1) /* A diagonal b (checkpoint > 1) não depende dos dois últimos checkpoints armazenados. */ if(lower==0) /* Preenche a célula 0 da diagonal b (somente delete). */ diag[b-inc][0].del=diag[b-inc-1][0].del+PT[b][PDD]; diag[b-inc][0].tb[0]=PDD; diag[b-inc][0].mat=(float)-INFINITO; diag[b-inc][0].tb[1]=10; diag[b-inc][0].ins=(float)-INFINITO; diag[b-inc][0].tb[2]=10; lower++; /* Preenche as demais células da diagonal b. */ for(t=lower;t<=upper;t++) if((b-t)>0) /* Preenche as células correspondentes às linhas b-1 até 1 na diagonal b. */ /* Preenche o campo delete da célula t da diagonal b. */ max=diag[b-inc-1][t].del+PT[b-t][PDD]; diag[b-inc][t].tb[0]=PDD; aux1=diag[b-inc-1][t].mat+PT[b-t][PMD]; aux2=diag[b-inc-1][t].ins+PT[b-t][PID]; if(max<=aux1) max=aux1; diag[b-inc][t].tb[0]=PMD; if(max<aux2) max=aux2; diag[b-inc][t].tb[0]=PID; diag[b-inc][t].del=max;
233
/* Preenche o campo match da célula t da diagonal b. */ max=diag[b-inc-2][t-1].del+PT[b-t][PDM]; diag[b-inc][t].tb[1]=PDM; aux1=diag[b-inc-2][t-1].mat+PT[b-t][PMM]; aux2=diag[b-inc-2][t-1].ins+PT[b-t][PIM]; if(max<=aux1) max=aux1; diag[b-inc][t].tb[1]=PMM; if(max<aux2) max=aux2; diag[b-inc][t].tb[1]=PIM; diag[b-inc][t].mat=mPE[b-t-1][IAA(As[t-1])]-f_aa[IAA(As[t-1])]+max; /* Preenche o campo insert da célula t da diagonal b. */ max=diag[b-inc-1][t-1].del+PT[b-t][PDI]; diag[b-inc][t].tb[2]=PDI; aux1=diag[b-inc-1][t-1].mat+PT[b-t][PMI]; aux2=diag[b-inc-1][t-1].ins+PT[b-t][PII]; if(max<=aux1) max=aux1; diag[b-inc][t].tb[2]=PMI; if(max<aux2) max=aux2; diag[b-inc][t].tb[2]=PII; diag[b-inc][t].ins=iPE[b-t][IAA(As[t-1])]-f_aa[IAA(As[t-1])]+max; /* Se b-t == 0 a célula pertence a linha 0 e recebe tratamento especial (somente insert). */ else /* Preenche a célula correspondentes à linha 0 na diagonal b. */ diag[b-inc][t].del=(float)-INFINITO; diag[b-inc][t].tb[0]=10; diag[b-inc][t].mat=(float)-INFINITO; diag[b-inc][t].tb[1]=10; diag[b-inc][t].ins=iPE[0][IAA(As[t-1])]-f_aa[IAA(As[t-1])]+diag[b-inc-1][t-1].ins+PT[0][PII]; diag[b-inc][t].tb[2]=PII; dec=dec+2; b--; if((b-inc-1)>=0) ackpt[b-inc-1]=ckpt-1; /* Diagonal ckpt-1 (ou b-1) armazenada no checkpoint b-inc-1. */ if((b-inc)>=0) ackpt[b-inc]=ckpt; /* Diagonal ckpt (ou b) armazenada no checkpoint b-inc. */ if(ckpt==d-1) ckpt++; else if((ckpt+CP-2-dec)<d-1) inc=ckpt+1; ckpt=ckpt+CP-2-dec; else inc=ckpt+1; ckpt=d-1; /* Conclui a fase forward --> Preenche o campo match da célula t da diagonal b. */ b++; max=diag[b-inc-1][aTS].del+PT[b-aTS][PDM]; origem=PDM; aux1=diag[b-inc-1][aTS].mat+PT[b-aTS][PMM]; aux2=diag[b-inc-1][aTS].ins+PT[b-aTS][PIM]; if(max<=aux1) max=aux1; origem=PMM; if(max<aux2) max=aux2; origem=PIM; logodds=max; escore=exp2(logodds); fprintf(align_out,"Escore Viterbi: seqüência com %d radicais -> %f (log-odds = %f).\n",aTS,escore,logodds); b--; /* Fase de retrocedimento sobre a matriz de Viterbi para recuperar o melhor caminho * Usar a função scanf() para imprimir os símbolos numa matriz (string) de char e * depois fazer as manipulações necessárias para imprimir corretamente num arquivo. */ ccko=dec; k=b-inc+1; b=NN+aTS+1; li=0; tt=aTS+1; teste=origem; ckpt=ckpt-k; /* Retrocede sobre a última seção da matriz de PD. */ /* Retrocede de NN+1 para NN ---> procedimento especial. */ if(teste==PDM || teste==PMM || teste==PIM) b--; tt--;k--; if(teste==PDM) fprintf(align_out,"D%d(-) ",b-tt); tcbk=diag[k][tt].tb[0]; else if(teste==PMM) fprintf(align_out,"M%d(%c) ",b-tt,As[tt-1]); tcbk=diag[k][tt].tb[1]; else fprintf(align_out,"I%d(%c) ",b-tt,As[tt-1]); tcbk=diag[k][tt].tb[2]; /* Retrocede sobre as demais linhas da seção. */ while(k>0) teste=tcbk; if(teste==PDD || teste==PMD || teste==PID) b--; k--; if(teste==PDD) fprintf(align_out,"D%d(-) ",b-tt); tcbk=diag[k][tt].tb[0]; else if(teste==PMD) fprintf(align_out,"M%d(%c) ",b-tt,As[tt-1]); tcbk=diag[k][tt].tb[1]; else fprintf(align_out,"I%d(%c) ",b-tt,As[tt-1]); tcbk=diag[k][tt].tb[2];
234
if(teste==PDM || teste==PMM || teste==PIM) if(k==1) k--; b--; flag=1; else b=b-2; tt--; k=k-2; if(teste==PDM) fprintf(align_out,"D%d(-) ",b-tt); tcbk=diag[k][tt].tb[0]; else if(teste==PMM) fprintf(align_out,"M%d(%c) ",b-tt,As[tt-1]); tcbk=diag[k][tt].tb[1]; else fprintf(align_out,"I%d(%c) ",b-tt,As[tt-1]); tcbk=diag[k][tt].tb[2]; if(teste==PDI || teste==PMI || teste==PII) b--; tt--; k--; if(teste==PDI) fprintf(align_out,"D%d(-) ",b-tt); tcbk=diag[k][tt].tb[0]; else if(teste==PMI) fprintf(align_out,"M%d(%c) ",b-tt,As[tt-1]); tcbk=diag[k][tt].tb[1]; else fprintf(align_out,"I%d(%c) ",b-tt,As[tt-1]); tcbk=diag[k][tt].tb[2]; while(ccko>=2) /* Recalcula as seções da matriz de PD durante a fase de retrocedimento. */ ccko=ccko-2; if(ccko>=2) k=CP-ccko; /* Tamanho do ciclo em cada seção. */ /* Limite inferior para os índices das diagonais no recalculo das seções da matriz PD. */ li=ckpt-k; ckpt=ckpt-k; for(j=li;j<b;j++) /* Caso o HMM seja maior do que a seqüência (NN > aTS). */ if(NN>aTS) if(j<=aTS) lower=0; upper=j; if(j>aTS && j<=NN) lower=0; upper=aTS; if(j>NN) lower=j-NN; upper=aTS; /* Caso o HMM seja igual a seqüência (NN = aTS). */ if(NN==aTS) if(j<=NN) lower=0; upper=j; else lower=j-NN; upper=aTS; /* Caso o HMM seja menor do que a seqüência (NN < aTS). */ if(NN<aTS) if(j<=NN) lower=0; upper=j; if(j<=aTS && j>NN) lower=j-NN; upper=j; if(j>aTS) lower=j-NN; upper=aTS; /* A próxima linha implementa o retrocedimento restrito. */ if(j>(b-tt)) lower=j-b+tt; if(upper>tt) upper=tt; low=lower; /* A diagonal j-li (checkpoint 0) depende dos dois últimos checkpoints armazenados. */ if((j-li)==0) if(lower==0) /* Preenche a célula 0 (linha j) da diagonal j (somente delete). */ diag[j-li][0].del=diag[k+1][0].del+PT[j][PDD]; diag[j-li][0].tb[0]=PDD; diag[j-li][0].mat=(float)-INFINITO; diag[j-li][0].tb[1]=10; diag[j-li][0].ins=(float)-INFINITO; diag[j-li][0].tb[2]=10; lower++; /* Preenche as demais células da diagonal j. */ for(t=lower;t<=upper;t++) if((j-t)>0) /* Preenche as células correspondentes às linhas l=j-1 até 1. */ /* Preenche o campo delete da célula t da diagonal j. */ max=diag[k+1][t].del+PT[j-t][PDD]; diag[j-li][t].tb[0]=PDD; aux1=diag[k+1][t].mat+PT[j-t][PMD]; aux2=diag[k+1][t].ins+PT[j-t][PID]; if(max<=aux1) max=aux1; diag[j-li][t].tb[0]=PMD; if(max<aux2) max=aux2; diag[j-li][t].tb[0]=PID; diag[j-li][t].del=max; /* Preenche o campo match da célula t da diagonal j. */
235
max=diag[k][t-1].del+PT[j-t][PDM]; diag[j-li][t].tb[1]=PDM; aux1=diag[k][t-1].mat+PT[j-t][PMM]; aux2=diag[k][t-1].ins+PT[j-t][PIM]; if(max<=aux1) max=aux1; if(j==2 && (j-t)==1) diag[j-li][t].tb[1]=9; else diag[j-li][t].tb[1]=PMM; if(max<aux2) max=aux2; diag[j-li][t].tb[1]=PIM; diag[j-li][t].mat=mPE[j-t-1][IAA(As[t-1])]-f_aa[IAA(As[t-1])]+max; /* Preenche o campo insert da célula t da diagonal j. */ max=diag[k+1][t-1].del+PT[j-t][PDI]; diag[j-li][t].tb[2]=PDI; aux1=diag[k+1][t-1].mat+PT[j-t][PMI]; aux2=diag[k+1][t-1].ins+PT[j-t][PII]; if(max<=aux1) max=aux1; diag[j-li][t].tb[2]=PMI; if(max<aux2) max=aux2; diag[j-li][t].tb[2]=PII; diag[j-li][t].ins=iPE[j-t][IAA(As[t-1])]-f_aa[IAA(As[t-1])]+max; /* Se j-t==0 a célula pertence a linha 0 e recebe tratamento especial (somente insert). */ else /* Preenche a célula correspondente à linha 0 na diagonal j. */ diag[j-li][t].del=(float)-INFINITO; diag[j-li][t].tb[0]=10; diag[j-li][t].mat=(float)-INFINITO; diag[j-li][t].tb[1]=10; diag[j-li][t].ins=iPE[0][IAA(As[t-1])]-f_aa[IAA(As[t-1])]+diag[k+1][t-1].ins+PT[0][PII]; diag[j-li][t].tb[2]=PII; if((j-li)==1) /* A diagonal j-inc (checkpoint 0) depende do último checkpoint armazenado. */ if(lower==0) /* Preenche a célula 0 (linha j) da diagonal j (somente delete). */ diag[j-li][0].del=diag[j-li-1][0].del+PT[j][PDD]; diag[j-li][0].tb[0]=PDD; diag[j-li][0].mat=(float)-INFINITO; diag[j-li][0].tb[1]=10; diag[j-li][0].ins=(float)-INFINITO; diag[j-li][0].tb[2]=10; lower++; /* Preenche as demais células da diagonal j. */ for(t=lower;t<=upper;t++) if((j-t)>0) /* Preenche as células correspondentes às linhas l=j-1 até 1. */ /* Preenche o campo delete da célula t da diagonal j. */ max=diag[j-li-1][t].del+PT[j-t][PDD]; diag[j-li][t].tb[0]=PDD; aux1=diag[j-li-1][t].mat+PT[j-t][PMD]; aux2=diag[j-li-1][t].ins+PT[j-t][PID]; if(max<=aux1) max=aux1; diag[j-li][t].tb[0]=PMD; if(max<aux2) max=aux2; diag[j-li][t].tb[0]=PID; diag[j-li][t].del=max; /* Preenche o campo match da célula t da diagonal j. */ max=diag[k+1][t-1].del+PT[j-t][PDM]; diag[j-li][t].tb[1]=PDM; aux1=diag[k+1][t-1].mat+PT[j-t][PMM]; aux2=diag[k+1][t-1].ins+PT[j-t][PIM]; if(max<=aux1) max=aux1; diag[j-li][t].tb[1]=PMM; if(max<aux2) max=aux2; diag[j-li][t].tb[1]=PIM; diag[j-li][t].mat=mPE[j-t-1][IAA(As[t-1])]-f_aa[IAA(As[t-1])]+max; /* Preenche o campo insert da célula t da diagonal j. */ max=diag[j-li-1][t-1].del+PT[j-t][PDI]; diag[j-li][t].tb[2]=PDI; aux1=diag[j-li-1][t-1].mat+PT[j-t][PMI]; aux2=diag[j-li-1][t-1].ins+PT[j-t][PII]; if(max<=aux1) max=aux1; diag[j-li][t].tb[2]=PMI; if(max<aux2) max=aux2; diag[j-li][t].tb[2]=PII; diag[j-li][t].ins=iPE[j-t][IAA(As[t-1])]-f_aa[IAA(As[t-1])]+max; /* Se j-t==0 a célula pertence a linha 0 e recebe tratamento especial (somente insert). */ else /* Preenche a célula correspondentes à linha 0 na diagonal j. */ diag[j-li][t].del=(float)-INFINITO; diag[j-li][t].tb[0]=10; diag[j-li][t].mat=(float)-INFINITO; diag[j-li][t].tb[1]=10; diag[j-li][t].ins=iPE[0][IAA(As[t-1])]-f_aa[IAA(As[t-1])]+diag[j-li-1][t-1].ins+PT[0][PII]; diag[j-li][t].tb[2]=PII;
236
/* A diagonal j (checkpoint > 1) não depende dos dois últimos checkpoints armazenados. */ if((j-li)>1) if(lower==0) /* Preenche a célula 0 da diagonal j (somente delete). */ diag[j-li][0].del=diag[j-li-1][0].del+PT[j][PDD]; diag[j-li][0].tb[0]=PDD; diag[j-li][0].mat=(float)-INFINITO; diag[j-li][0].tb[1]=10; diag[j-li][0].ins=(float)-INFINITO; diag[j-li][0].tb[2]=10; lower++; /* Preenche as demais células da diagonal j. */ for(t=lower;t<=upper;t++) if((j-t)>0) /* Preenche as células correspondentes às linhas l=j-1 até 1. */ /* Preenche o campo delete da célula t da diagonal j. */ max=diag[j-li-1][t].del+PT[j-t][PDD]; diag[j-li][t].tb[0]=PDD; aux1=diag[j-li-1][t].mat+PT[j-t][PMD]; aux2=diag[j-li-1][t].ins+PT[j-t][PID]; if(max<=aux1) max=aux1; diag[j-li][t].tb[0]=PMD; if(max<aux2) max=aux2; diag[j-li][t].tb[0]=PID; diag[j-li][t].del=max; /* Preenche o campo match da célula t da diagonal j. */ max=diag[j-li-2][t-1].del+PT[j-t][PDM]; diag[j-li][t].tb[1]=PDM; aux1=diag[j-li-2][t-1].mat+PT[j-t][PMM]; aux2=diag[j-li-2][t-1].ins+PT[j-t][PIM]; if(max<=aux1) max=aux1; diag[j-li][t].tb[1]=PMM; if(max<aux2) max=aux2; diag[j-li][t].tb[1]=PIM; diag[j-li][t].mat=mPE[j-t-1][IAA(As[t-1])]-f_aa[IAA(As[t-1])]+max; /* Preenche o campo insert da célula t da diagonal j. */ max=diag[j-li-1][t-1].del+PT[j-t][PDI]; diag[j-li][t].tb[2]=PDI; aux1=diag[j-li-1][t-1].mat+PT[j-t][PMI]; aux2=diag[j-li-1][t-1].ins+PT[j-t][PII]; if(max<=aux1) max=aux1; diag[j-li][t].tb[2]=PMI; if(max<aux2) max=aux2; diag[j-li][t].tb[2]=PII; diag[j-li][t].ins=iPE[j-t][IAA(As[t-1])]-f_aa[IAA(As[t-1])]+max; /* Se j-t==0 a célula pertence a linha 0 e recebe tratamento especial (somente insert). */ else /* Preenche a célula correspondentes à linha 0 na diagonal j. */ diag[j-li][t].del=(float)-INFINITO; diag[j-li][t].tb[0]=10; diag[j-li][t].mat=(float)-INFINITO; diag[j-li][t].tb[1]=10; diag[j-li][t].ins=iPE[0][IAA(As[t-1])]-f_aa[IAA(As[t-1])]+diag[j-li-1][t-1].ins+PT[0][PII]; diag[j-li][t].tb[2]=PII; /* Retrocede sobre as seções da matriz de PD. */ if(flag==1) b--; tt--; k--; flag=0; if(teste==PDM) fprintf(align_out,"D%d(-) ",b-tt); tcbk=diag[k][tt].tb[0]; else if(teste==PMM) fprintf(align_out,"M%d(%c) ",b-tt,As[tt-1]); tcbk=diag[k][tt].tb[1]; else fprintf(align_out,"I%d(%c) ",b-tt,As[tt-1]); tcbk=diag[k][tt].tb[2]; while(k>0) teste=tcbk; if(teste==PDD || teste==PMD || teste==PID) b--; k--; if(teste==PDD) fprintf(align_out,"D%d(-) ",b-tt); tcbk=diag[k][tt].tb[0]; else if(teste==PMD) fprintf(align_out,"M%d(%c) ",b-tt,As[tt-1]); tcbk=diag[k][tt].tb[1]; else fprintf(align_out,"I%d(%c) ",b-tt,As[tt-1]); tcbk=diag[k][tt].tb[2]; if(teste==PDM || teste==PMM || teste==PIM) if(k==1) k--; b--; flag=1; else b=b-2; tt--; k=k-2; if(teste==PDM) fprintf(align_out,"D%d(-) ",b-tt); tcbk=diag[k][tt].tb[0];
237
else if(teste==PMM) fprintf(align_out,"M%d(%c) ",b-tt,As[tt-1]); tcbk=diag[k][tt].tb[1]; else fprintf(align_out,"I%d(%c) ",b-tt,As[tt-1]); tcbk=diag[k][tt].tb[2]; if(teste==PDI || teste==PMI || teste==PII) b--; tt--; k--; if(teste==PDI) fprintf(align_out,"D%d(-) ",b-tt); tcbk=diag[k][tt].tb[0]; else if(teste==PMI) fprintf(align_out,"M%d(%c) ",b-tt,As[tt-1]); tcbk=diag[k][tt].tb[1]; else fprintf(align_out,"I%d(%c) ",b-tt,As[tt-1]); tcbk=diag[k][tt].tb[2]; /* Retrocedimento sobre as diagonais 0 e 1 da matriz_PD --> tratamento especial. */ teste=tcbk; if(b==2 && tt==0) if(teste==PDD) b--; fprintf(align_out,"D%d(-) ",b-tt); if(b==2 && tt==1) if(teste==PDI) b--; tt--; fprintf(align_out,"D%d(-) ",b-tt); if(teste==PID) b--; fprintf(align_out,"I%d(%c) ",b-tt,As[tt-1]); if(b==2 && tt==2) if(teste==PII) b--; tt--; fprintf(align_out,"I%d(%c) ",b-tt,As[tt-1]); fprintf(align_out,"\n"); /* Efetua a tomada de tempo no final do alinhameto da seqüência. */ hpctim(&wtimes, &ctimes, &wbegins, &cbegins); printf("WALL time gasto no alinhamento --> %f.\n", wtimes); printf("CPU time gasto no alinhamento --> %f.\n", ctimes); fprintf(align_out,"WALL time gasto no alinhamento da seqüência --> %f.\n",wtimes); fprintf(align_out,"CPU time gasto no alinhamento da seqüência --> %f.\n",ctimes); /* Fechado o arquivo de saída para gravar o escore e o alimhamento. */ if(fclose(align_out)!=0) perror("Erro fechando arquivo de saída do alinhamento."); exit(0); return(4);
238
APÊNDICE 4
ALGORITMO DE VITERBI COM 2-NÍVEIS DE CHECKPOINTS BIDIMENSIONAIS, PARTICIONAMENTO FIXO DE MEMÓRIA E
RETROCEDIMENTO RESTRITO
/* **************************************************************************************** * Algoritmo de Viterbi para alinhar seqüências de aminoácidos * Copyright (C) 2003 José Olimpio Ferreira * Instituto Nacional de Pesquisas Espaciais e * Universidade Catolica de Goias * All Rights Reserved * **************************************************************************************** * * align_vit_ckpts_bid_uni.c * * --> Algoritmo de Viterbi para obter o melhor alinhamento de seqüências de aminoácidos a um HMM. * --> Este é um algoritmo de 2-níveis com checkpoints (completos) bidimensionais * por linhas x colunas (padrão) * --> Requer espaço da ordem O(2*m*SQRT(n) + 2*n*SQRT(m)). * --> É útil para alinhamento de seqüências e modelos grandes * --> Tamanhos mínimos de seqüências e modelos --> 50 * **************************************************************************************** * * Entradas: * --> Arquivo de probabilidades de transição de estados --> hmm[nos].transicoes * --> Arquivo de probabilidades de emissão de símbolos --> hmm[nos].emissoes * --> Arquivo de seqüências para alinhamento ao HMM --> <nome_arquivo> * --> nós = tamnho do HMM * * Saída: * --> Escore e alinhamento Viterbi da seqüência ao HMM --> hmm[nos].upalign * * Compilação - Linkedição - Construção do executavel ---> exemplo para intel pentium 3 * --> gcc -c -march=pentium3 -mcpu=pentium3 -mfpmath=sse -O3 align_vit_ckpts_bid_uni.c * --> gcc -c -march=pentium3 -mcpu=pentium3 -mfpmath=sse -O3 hpctim.c * --> gcc -c -march=pentium3 -mcpu=pentium3 -mfpmath=sse -O3 hpcwall.c * --> gcc -c -march=pentium3 -mcpu=pentium3 -mfpmath=sse -O3 etime.c * --> gcc -o align_vit_ckpts_bid_uni align_vit_ckpts_bid_uni.o hpctim.o hpcwall.o etime.o -lm * * Compilação - Linkedição - Construção do executavel ---> exemplo para amd athlon k7 * --> gcc -c -march=athlon -mcpu=athlon -mfpmath=387 -O3 align_vit_ckpts_bid_uni.c * --> gcc -c -march=athlon -mcpu=athlon -mfpmath=387 -O3 hpctim.c * --> gcc -c -march=athlon -mcpu=athlon -mfpmath=387 -O3 hpcwall.c * --> gcc -c -march=athlon -mcpu=athlon -mfpmath=387 -O3 etime.c * --> gcc -o align_vit_ckpts_bid_uni align_vit_ckpts_bid_uni.o hpctim.o hpcwall.o etime.o -lm * **************************************************************************************** */ #include <stdio.h> #include <stdlib.h> #include <math.h> #include <time.h> #include <string.h> /*definie algumas macros */ #define IAA(x) (strchr(aa, (x)) - aa) /* Índice de símbolos de aminoácidos. */ #define log2(x) ((x)>0.0 ? log(x)*1.44269504 : -19) /* logaritmo de base 2. */ #define exp2(x) (exp(x)*0.69314718) /* exponencial de base 2. */ #define INFINITO 99999999 /* infinito para log-probilidades de valores zero. */ #define aa_alfabeto "ACDEFGHIKLMNPQRSTVWY" /* Alfabeto não-degenerado de aminoácidos. */
239
/* String contendo os símbolos não-degenerados dos aminoácidos - Alfabeto de aminoácidos. */ const char aa[21] = aa_alfabeto; /* Índices para as probabilidades de transição de estado do HMM. */ enum transicoes_indices PDD=0,PMD=1,PID=2,PDM=3,PMM=4,PIM=5,PDI=6,PMI=7,PII=8; /* Váriaveis: * NS --> número de seqüências. * TS --> tamanho da maior seqüência. * NN --> número de nós do HMM. * Model --> HMM 500,1000,2000,4000,8000 nós. * Arquivo --> nome do arquivo que contém as seqüências no formato HMMpro. */ const long int NS, TS, NN; char Model[80], Arquivo[80]; /* --> Freqüências f(i) - Dayhoff - de occorrência dos 20 aminoácidos. * (função massa de probabilidade para aminoácidos). * --> Obtidos do BD SwissProt 34: considerando 21.210.388 raidicais. * --> Apresentadas em ordem alfabética por código de letras simples. * --> Usadas como probabilidades de emissão de símbolos no modelo nulo ou aleatorio (R). */ const float f_aa[20] = (float)-3.72699742, /* (float)0.075520, /* A */ (float)-5.88061460, /* (float)0.016973, /* C */ (float)-4.23707464, /* (float)0.053029, /* D */ (float)-3.98384032, /* (float)0.063204, /* E */ (float)-4.61663135, /* (float)0.040762, /* F */ (float)-3.86884780, /* (float)0.068448, /* G */ (float)-5.47997107, /* (float)0.022406, /* H */ (float)-4.12572395, /* (float)0.057284, /* I */ (float)-4.07344183, /* (float)0.059398, /* K */ (float)-3.42044908, /* (float)0.093399, /* L */ (float)-5.40696564, /* (float)0.023569, /* M */ (float)-4.46456809, /* (float)0.045293, /* N */ (float)-4.34338099, /* (float)0.049262, /* P */ (float)-4.63554859, /* (float)0.040231, /* Q */ (float)-4.27724022, /* (float)0.051573, /* R */ (float)-3.79157763, /* (float)0.072214, /* S */ (float)-4.12144885, /* (float)0.057454, /* T */ (float)-3.93783407, /* (float)0.065252, /* V */ (float)-6.32042847, /* (float)0.012513, /* W */ (float)-4.96646070 /* (float)0.031985 /* Y */ ; /* Ponteiros para arquivos. */ FILE *seq_in, *pt_in, *pe_in, *align_out, *scores_out; /* Declaração dos protótipos das funções internas. */ int analisa_arq_seq(long int vseq[4]); int read_seq (char As[TS+1]); int read_model (float PT[][9], float mPE[][20], float iPE[][20]); int viterbi_L2_ckpt_bid(float PT[][9],float mPE[][20],float iPE[][20],char As[TS+1]); /* Declaração dos protótipos das funções externas. */ void etime(float *result); void hpcwall(double *retval); void hpctim(float *wtime, float *ctime, float *wbegin, float *cbegin); /* FUNÇÃO PRINCIPAL --> MAIN */ int main (int argc, char *argv[]) long int i, j, vseq[4]=0,0,0,0, *ptr; int status; float *ptrf; /* Trata os argumentos da função principal. */ if(argc!=3) printf("\nVocê esqueceu de informar:\n"); printf("o tamanho do HMM (Exemplo: 100, 500, 1000, 2000, 4000),\n"); printf("o nome do arquivo contendo as seqüências.\n\n"); printf("Entre: <nome_programa> <tamanho_HMM> <nome_arquivo>.\n"); printf("O arquivo deve estar no formato HMMpro --> http://www.netid.com.\n"); exit(1); /* Inicializa algumas variáveis globais do tipo char* e const int. Usadas como informações de nomes de arquivos, de tamanho do modelo e de dimensões de matrizes. */ strcpy(Model,argv[1]);
240
(const) ptr = &NN; *ptr = atoi(argv[1]); strcpy(Arquivo,argv[2]); /* Chamada para a função análise das seqüências. */ status = analisa_arq_seq(vseq); /* Verifica o status do retorno da função analisa_arq_seq. */ if(status!=1) printf("Erro na análise das sequecias.\n"); return(0); (const) ptr = &NS; *ptr = vseq[0]; (const) ptr = &TS; *ptr = vseq[1]; /* Matriz usada para armazenar as seqüências a serem alinhadas. */ char strAs[TS+1]; /* Matriz de probabilidades de transição do HMM. */ float pt_hmm[NN+2][9]; for(i=0;i<NN+2;i++) for(j=0;j<9;j++) pt_hmm[i][j] = (float)0.0; /* Matriz de probabilidades de emissão de símbolos dos estados MATCH. */ float mf_aa[NN][20]; for(i=0;i<NN;i++) for(j=0;j<20;j++) mf_aa[i][j] = 0.0; /* Matriz de probabilidades de emissão de símbolos dos estados INSERT */ float if_aa[NN+1][20]; for(i=0;i<NN+1;i++) for(j=0;j<20;j++) if_aa[i][j] = 0.0; /* Chamada para a função de leitura da seqüência. */ status = read_seq(strAs); /* Verifica o status do retorno da função read_seq. */ if(status!=2) printf("Erro na leitura da seqüência.\n"); return(0); /* Chamada para a função de leitura de parâmetros do HMM. */ status = read_model(pt_hmm, mf_aa, if_aa); /* Verifica o status do retorno da função read_model. */ if(status!=3) printf("Erro na leitura dos parametros do HMM.\n"); return(0); /* Converte probabilidades para log-probabilidades. */ for(i=0;i<NN+2;i++) for(j=0;j<9;j++) pt_hmm[i][j] = log2(pt_hmm[i][j]); for(i=0;i<NN;i++) for(j=0;j<20;j++) if_aa[i][j] = log2(if_aa[i][j]); for(j=0;j<20;j++) mf_aa[i][j] = log2(mf_aa[i][j]); for(j=0;j<20;j++) if_aa[i][j] = log2(if_aa[i][j]); /* Chamada para a função de alinhamento da seqüência ao HMM. */ status = viterbi_L2_ckpt_bid(pt_hmm, mf_aa, if_aa, strAs); /* Verifica o status do retorno da função viterbi_L2_ckpt_bid */ if(status!=4) printf("Erro no alinhamento da seqüência ao HMM.\n"); return(0); return (0); /* FUNÇÃO DE ANÁLISE ARQUIVO DE SEQÜÊNCIAS. */ int analisa_arq_seq(long int vseq[4]) int i,j,k; long int count=0, soma=0, max=1024000, maximo=0,minimo=1024000, media=0; char string[1024000]; /* Abindo arquivo de seqüências para leitura. */ seq_in=fopen(Arquivo,"r"); if(seq_in==NULL) perror("Erro abrindo arquivo de seqüências.\n"); exit(0); /* Verificando o cabeçalho de formato do arquivo. */ fgets(string,max,seq_in); if(j=strcmp(string,"ObjectData:\n")) printf("(1) Problemas no formato do arquivo --> (%d).\n",j); exit(1); fgets(string,max,seq_in); if(j=strcmp(string,"DATA: string\n")) printf("(2) Problemas no formato do arquivo --> (%d).\n",j); exit(1); fgets(string,max,seq_in); if(j=strcmp(string,"\n")) printf("(3) Problemas no formato do arquivo --> (%d).\n",j); exit(1); i=0;j=1; /* Obendo estatísticas das seqüências. */ while(j) fgets(string,max,seq_in); if(string[0]!='\n' && string[0]!='\r' && string[0]!=' ' && !feof(seq_in)) count = strlen(string)-1; soma += count; if(count>maximo) maximo=count; if(count<minimo && count!=1) minimo=count; i++; string[0]=' ';
241
else j=0; fprintf(stdout,"NS = %d, TS = %d, Nos do HMM = %s.\n",i,maximo,Model); media = (int) soma/i; vseq[0] = i; vseq[1] = maximo+1; vseq[2] = minimo; vseq[3] = media; return(1); /* FUNÇÃO DE LEITURA DA SEQÜÊNCIA. */ int read_seq (char As[TS+1]) int i,j; char string[TS+1]; /* reinicializa o indicador de posição do arquivo. */ rewind(seq_in); if(seq_in==NULL) perror("Erro reinicializando indicador de posição.\n"); exit(0); /* Copiando as seqüências do arquivo para uma matriz de strings. */ for(i=0;i<3;i++) fgets(string,TS+1,seq_in); fgets(As,TS+1,seq_in); j=(int)strlen(As); As[j-1]='\0'; /* Fechado o arquivo da seqüência. */ if(fclose(seq_in)!=0) perror("Erro fechando arquivo da seqüência. "); exit(0); return(2); /* FUNÇÃO DE LEITURA DOS PARÂMETROS DO MODELO */ int read_model (float PT[][9], float mPE[][20], float iPE[][20]) char nome_arquivo[20] = "hmm", string[TS+1]; long int i, j, state; /* Abre arquivo de probabilidades de transições de estados para leitura. */ strcat(nome_arquivo, Model); strcat(nome_arquivo, ".transicoes"); pt_in=fopen(nome_arquivo,"r"); if(pt_in==NULL) perror("Erro abrindo arquivo de Probabiledades de Transições do HMM..\n"); exit(0); /* Copiando as probabilidades do arquivo para a matriz de Probilidades de Transições. */ /* Lendo as 2 primeiras linhas do arquivo (bloco 0 do HMM) e preenchendo as 2 primeiras linhas da matriz. */ fscanf(pt_in,"%d%f%f%f",&state,&PT[1][4],&PT[0][7],&PT[1][1]); fscanf(pt_in,"%d%f%f%f",&state,&PT[1][5],&PT[0][8],&PT[1][2]); /* Lendo as proximas 3*NN-2 linhas do arquivo (blocos de 1 a NN-1 do HMM) e preenchendo as próximas NN-1 linhas da matriz. */ for(i=1;i<NN;i++) fscanf(pt_in,"%d%f%f%f",&state,&PT[i+1][3],&PT[i][6],&PT[i+1][0]); fscanf(pt_in,"%d%f%f%f",&state,&PT[i+1][4],&PT[i][7],&PT[i+1][1]); fscanf(pt_in,"%d%f%f%f",&state,&PT[i+1][5],&PT[i][8],&PT[i+1][2]); /* Lendo as 2 últimas linhas do arquivo e preenchendo as 2 últimas linhas da matriz. */ fscanf(pt_in,"%d%f%f",&state,&PT[NN+1][3],&PT[NN][6]); fscanf(pt_in,"%d%f%f",&state,&PT[NN+1][4],&PT[NN][7]); fscanf(pt_in,"%d%f%f",&state,&PT[NN+1][5],&PT[NN][8]); /* Fechado o arquivo de probabilidades de transições. */ if(fclose(pt_in)!=0) perror("Erro fechando arquivo de Probilidades de Transições."); exit(0); /* Abre arquivo de probabilidades de emissoes de símbolos para leitura. */ strcpy(nome_arquivo,"hmm\0"); strcat(nome_arquivo, Model); strcat(nome_arquivo, ".emissoes"); pe_in=fopen(nome_arquivo,"r"); if(pe_in==NULL) perror("Erro abrindo arquivo de Probabiledades de Emissoes do HMM.\n"); exit(0); /* Copiando as probabilidades do arquivo para a matriz de Probilidades de Emissoes. */ for(i=0;i<NN;i++) fscanf(pe_in,"%d",&state); for(j=0;j<20;j++) fscanf(pe_in,"%f",&iPE[i][j]); fscanf(pe_in,"%d",&state); for(j=0;j<20;j++) fscanf(pe_in,"%f",&mPE[i][j]); fscanf(pe_in,"%d",&state); for(j=0;j<20;j++) fscanf(pe_in,"%f",&iPE[NN][j]); /* Fechado o arquivo de probabilidades de emissões de símbolos. */
242
if(fclose(pe_in)!=0) perror("Erro fechando arquivo de Probilidades de Emissoes."); exit(0); return(3); /* FUNÇÃO ALINHA SEQÜÊNCIAS --> VITERBI 2-NÏVEIS DE CHECKPOINTS BIDIMENSIONAIS. */ int viterbi_L2_ckpt_bid(float PT[][9],float mPE[][20],float iPE[][20],char As[TS+1]) long int s, kk, t, tt, b, bt, aTS, nn, ats; char nome_arquivo[20] = "hmm", ch[5]; float logodds, escore, max, aux1, aux2; double aux3; int i, k, l, j, ki, kj, ii, dd, tcbk, teste, origem, CP, RL, RC, il, ic, src, trt; /* Váriaveis usadas para a medição de tempo. */ float wtimes=0.0, ctimes=0.0, wbegins=0.0, cbegins=0.0; /* Obtém o tamanho da seqüência s. */ aTS=strlen(As); /* Determina a quantidade de linhas de checkpoints necessárias. */ CP=(int)sqrt((double)NN); RL=(int)2*CP-1; /* Número de linhas de checkpoint.s */ CP=(int)sqrt((double)aTS); RC=(int)2*CP-1; /* Número de colunas de checkpoints. */ /* Intervalo entre dois checkpoints-linha sucessivos. */ aux1=(float)NN/RL; aux2=(float)modf((double)aux1,&aux3); il=(int)aux3; if(aux2>=0.5) il++; while(il*RL>=NN) RL--; /* Intervalo entre dois checkpoints-coluna sucessivos. */ aux1=(float)(aTS+1)/RC; aux2=(float)modf((double)aux1,&aux3); ic=(int)aux3; if(aux2>=0.5) ic++; while(ic*RC>=aTS) RC--; /* Struct de células da Matriz de Viterbi. */ /* Matrizes de checkpoints para armazenar células da Matriz de Viterbi. */ struct celula_PD float del, mat, ins; /* Guarda os escores dos estados delete, insert e match. */ int tb[3]; /* Guarda o estado anterior no caminho Viterbi: delete=0, match=1 e insert=2. */ linha[RL+1][aTS+1],coluna[NN+1][RC+1]; /* Linha/Coluna extra para armazenar a linha/coluna zero. */ /* Matriz de Checkpoints de Trabalho para calcular linhas/seções-linha da Matriz de Viterbi. */ union ckpt_trab struct celula_PD fw[2][aTS+1]; struct celula_PD bw[2*il][2*ic]; align; /* Inicializa rotina de tempo. */ hpctim(&wtimes,&ctimes,&wbegins,&cbegins); hpctim(&wtimes,&ctimes,&wbegins,&cbegins); /* Inicializa as matrizes de checkpoints linha/colunas (de escores Viterbi). */ for(b=0;b<RL+1;b++) for(t=0;t<aTS+1;t++) linha[b][t].del = 0.0; linha[b][t].mat = 0.0; linha[b][t].ins = 0.0; linha[b][t].tb[0] = 11; linha[b][t].tb[1] = 11; linha[b][t].tb[2] = 11; for(b=0;b<NN+1;b++) for(t=0;t<RC+1;t++) coluna[b][t].del = 0.0; coluna[b][t].mat = 0.0; coluna[b][t].ins = 0.0; coluna[b][t].tb[0] = 11; coluna[b][t].tb[1] = 11; coluna[b][t].tb[2] = 11; /* Inicializa as matrizes da união ckpt_trab. */ for(b=0;b<2;b++) for(t=0;t<aTS+1;t++) align.fw[b][t].del = 0.0; align.fw[b][t].mat = 0.0; align.fw[b][t].ins = 0.0; align.fw[b][t].tb[0] = 11; align.fw[b][t].tb[1] = 11; align.fw[b][t].tb[2] = 11; /* Abre arquivo para escrita dos alinhamentos. */ strcat(nome_arquivo, Model);
243
strcat(nome_arquivo, ".upalign"); align_out = fopen(nome_arquivo,"w"); if(align_out == NULL) perror("Erro abrindo arquivo de Alinhamentos do HMM.\n"); exit(0); /* Efetua o alinhamento da seqüência ao modelo e escreve este para um arquivo. */ fprintf(align_out,"Alinhamento da seqüência com %d radicais ao HMM com %s nós.\n",aTS,Model); fprintf(align_out,"Algoritmo de viterbi de 2-níveis de checkpoints bidimensionais (padrão).\n"); /* Efetua o calculo da memoria mínima requerida. */ aux1=(float) 18*RL*aTS+18*RC*NN+2*18*aTS; aux2=aux1/1024; max=aux2/1024; fprintf(align_out,"A memória mínima alocada é de %f bytes (= %f Kb = %f Mb).\n",aux1,aux2,max); printf("A memória mínima alocada é de %f bytes (= %f Kb = %f Mb).\n",aux1,aux2,max); /* Efetua a tomada de tempo no início do alinhameto da seqüência. */ hpctim(&wtimes, &ctimes, &wbegins, &cbegins); /* Inicia a fase de cálculos para frente --> FORWARD. */ /* Enche a linha 0 da matriz de Viterbi para a seqüência s (somente insert). */ /* Primeiro Enche as células 0 e 1. */ align.fw[0][0].del=(float)-INFINITO; align.fw[0][0].mat=0.0; align.fw[0][0].ins=(float)-INFINITO; align.fw[0][0].tb[0]=10; align.fw[0][0].tb[1]=9; align.fw[0][0].tb[2]=10; align.fw[0][1].del=(float)-INFINITO;
linha[0][t].del = align.fw[0][t].del;
coluna[0][j].ins = align.fw[0][kk].ins;
align.fw[trt][0].mat=(float)-INFINITO;
align.fw[0][1].mat=(float)-INFINITO; align.fw[0][1].ins=iPE[0][IAA(As[0])]-f_aa[IAA(As[0])]+PT[0][PMI]; align.fw[0][1].tb[0]=10; align.fw[0][1].tb[1]=10; align.fw[0][1].tb[2]=9; /* Depois as células de t=2 ate aTS da linha 0. */ for(t=2;t<=aTS;t++) align.fw[0][t].del=(float)-INFINITO; align.fw[0][t].mat=(float)-INFINITO; align.fw[0][t].ins=iPE[0][IAA(As[t-1])]-f_aa[IAA(As[t-1])]+align.fw[0][t-1].ins+PT[0][PII]; align.fw[0][t].tb[0]=10; align.fw[0][t].tb[1]=10; align.fw[0][t].tb[2]=PII; /* Salva a linha 0 no checkpoint-linha 0. */ for(t=0;t<=aTS;t++)
linha[0][t].mat = align.fw[0][t].mat; linha[0][t].ins = align.fw[0][t].ins; linha[0][t].tb[0] = align.fw[0][t].tb[0]; linha[0][t].tb[1] = align.fw[0][t].tb[1]; linha[0][t].tb[2] = align.fw[0][t].tb[2]; /* Salva a posição (linha) 0 dos checkpoints-colunas. */ for(kk=0;kk<=RC*ic;kk=kk+ic) j=(int)kk/ic; coluna[0][j].del = align.fw[0][kk].del; coluna[0][j].mat = align.fw[0][kk].mat;
coluna[0][j].tb[0] = align.fw[0][kk].tb[0]; coluna[0][j].tb[1] = align.fw[0][kk].tb[1]; coluna[0][j].tb[2] = align.fw[0][kk].tb[2]; /* Enche as demais linhas da matriz de Viterbi para a seqüência s. */ i=1; for(b=1;b<=NN;b++) src=(b-1)%2; trt=b%2; /* Preenche a célula 0 da linha b (somente delete). */ if(b==1) align.fw[trt][0].del=align.fw[src][0].mat+PT[b][PMD]; align.fw[trt][0].tb[0]=9; else align.fw[trt][0].del=align.fw[src][0].del+PT[b][PDD]; align.fw[trt][0].tb[0]=PDD;
align.fw[trt][0].ins=(float)-INFINITO;
244
align.fw[trt][0].tb[1]=10; align.fw[trt][0].tb[2]=10; /* Preenche as demais células da linha b. */ for(t=1;t<=aTS;t++) /* Preenche o campo delete da célula t da linha b. */ max= align.fw[src][t].del+PT[b][PDD]; aux1=align.fw[src][t].mat+PT[b][PMD]; aux2=align.fw[src][t].ins+PT[b][PID]; align.fw[trt][t].tb[0]=PDD; if(max<=aux1) max=aux1; align.fw[trt][t].tb[0]=PMD; if(max<aux2) max=aux2; align.fw[trt][t].tb[0]=PID;
coluna[b][j].ins = align.fw[trt][kk].ins;
linha[i][t].tb[1] = align.fw[trt][t].tb[1];
if(max<aux2) max=aux2; origem=PIM;
align.fw[trt][t].del=max; /* Preenche o campo match da célula t da linha b. */ max= align.fw[src][t-1].del+PT[b][PDM]; aux1=align.fw[src][t-1].mat+PT[b][PMM]; aux2=align.fw[src][t-1].ins+PT[b][PIM]; align.fw[trt][t].tb[1]=PDM; if(max<=aux1) max=aux1; if(b==1) align.fw[trt][t].tb[1]=9; else align.fw[trt][t].tb[1]=PMM; if(max<aux2) max=aux2; align.fw[trt][t].tb[1]=PIM; align.fw[trt][t].mat=mPE[b-1][IAA(As[t-1])]-f_aa[IAA(As[t-1])]+max; /* Preenche o campo insert da célula t da linha b. */ max= align.fw[trt][t-1].del+PT[b][PDI]; aux1=align.fw[trt][t-1].mat+PT[b][PMI]; aux2=align.fw[trt][t-1].ins+PT[b][PII]; align.fw[trt][t].tb[2]=PDI; if(max<=aux1) max=aux1; align.fw[trt][t].tb[2]=PMI; if(max<aux2) max=aux2; align.fw[trt][t].tb[2]=PII; align.fw[trt][t].ins=iPE[b][IAA(As[t-1])]-f_aa[IAA(As[t-1])]+max; /* Salva a posição (linha) b dos checkpoints-colunas. */ for(kk=0;kk<=RC*ic;kk=kk+ic) j=(int)kk/ic; coluna[b][j].del = align.fw[trt][kk].del; coluna[b][j].mat = align.fw[trt][kk].mat;
coluna[b][j].tb[0] = align.fw[trt][kk].tb[0]; coluna[b][j].tb[1] = align.fw[trt][kk].tb[1]; coluna[b][j].tb[2] = align.fw[trt][kk].tb[2]; /* Salva as linhas i*il (i=1,2,..,RL) nos checkpoints-linha il(s). */ if(b==i*il && i<=RL) for(t=0;t<=aTS;t++) linha[i][t].del = align.fw[trt][t].del; linha[i][t].mat = align.fw[trt][t].mat; linha[i][t].ins = align.fw[trt][t].ins; linha[i][t].tb[0] = align.fw[trt][t].tb[0];
linha[i][t].tb[2] = align.fw[trt][t].tb[2]; i++; /* Conclui a fase forward --> Preenche o campo match da célula t da linha b. */ max= align.fw[trt][t-1].del+PT[b][PDM]; aux1=align.fw[trt][t-1].mat+PT[b][PMM]; aux2=align.fw[trt][t-1].ins+PT[b][PIM]; origem=PDM; if(max<=aux1) max=aux1; origem=PMM;
logodds=max; escore=exp2(logodds); fprintf(align_out,"Escore Viterbi: seqüência com %d radicais -> %f (log-odds = %f).\n",aTS,escore,logodds); /* Fase de retrocedimento (Restrito) sobre a matriz de Viterbi para recuperar o melhor caminho. Primeiro --> faz-se as recomputações da última seção - a seção do canto inferior direiro, que contém a célula (NN,aTS) com o maior escore. Segundo --> faz-se o retrocedimento nesta seção. Passa-se entao para a próxima seção e procede com os passos Primeiro e Segundo. Prosegue desta forma ate alcancar a célula (0,0) no canto superior esquerdo.
245
*/ tcbk=origem; ki=0; kj=0; nn=NN; ats=aTS; bt=NN+1; tt=aTS+1; i=il*RL; j=ic*RC; ii=0; while(tcbk!=9 && ii==0) /* Recomputações de seções da matriz de Viterbi para a seqüência. */ /* Copia a seção do checkpoint-linha RL-ki (linha[RL-ki][:]) na matriz-seção align.bw[0][:]. */ for(t=j;t<=ats;t++) align.bw[0][t-j].del = linha[RL-ki][t].del; align.bw[0][t-j].mat = linha[RL-ki][t].mat; align.bw[0][t-j].ins = linha[RL-ki][t].ins; align.bw[0][t-j].tb[0] = linha[RL-ki][t].tb[0]; align.bw[0][t-j].tb[1] = linha[RL-ki][t].tb[1]; align.bw[0][t-j].tb[2] = linha[RL-ki][t].tb[2]; /* Copia a seção do checkpoint-coluna RC-ki (coluna[:][RC-ki]) na matriz-seção align.bw[:][0]. */
align.bw[b-i][0].tb[1] = coluna[b][RC-kj].tb[1];
for(b=i+1;b<=nn;b++) align.bw[b-i][0].del = coluna[b][RC-kj].del; align.bw[b-i][0].mat = coluna[b][RC-kj].mat; align.bw[b-i][0].ins = coluna[b][RC-kj].ins; align.bw[b-i][0].tb[0] = coluna[b][RC-kj].tb[0];
align.bw[b-i][0].tb[2] = coluna[b][RC-kj].tb[2]; /* Enche as demais linhas/colunas das seções da matriz de Viterbi para a seqüência. */ for(b=i+1;b<=nn;b++) for(t=j+1;t<=ats;t++) /* Preenche o campo delete da célula t da linha b. */ max= align.bw[b-i-1][t-j].del+PT[b][PDD]; aux1=align.bw[b-i-1][t-j].mat+PT[b][PMD]; aux2=align.bw[b-i-1][t-j].ins+PT[b][PID]; align.bw[b-i][t-j].tb[0]=PDD; if(max<=aux1) max=aux1; align.bw[b-i][t-j].tb[0]=PMD; if(max<aux2) max=aux2; align.bw[b-i][t-j].tb[0]=PID; align.bw[b-i][t-j].del=max; /* Preenche o campo match da célula t da linha b. */ max= align.bw[b-i-1][t-j-1].del+PT[b][PDM]; aux1=align.bw[b-i-1][t-j-1].mat+PT[b][PMM]; aux2=align.bw[b-i-1][t-j-1].ins+PT[b][PIM]; align.bw[b-i][t-j].tb[1]=PDM; if(max<=aux1) max=aux1; if(b==1) align.bw[b-i][t-j].tb[1]=9; else align.bw[b-i][t-j].tb[1]=PMM; if(max<aux2) max=aux2; align.bw[b-i][t-j].tb[1]=PIM; align.bw[b-i][t-j].mat=mPE[b-1][IAA(As[t-1])]-f_aa[IAA(As[t-1])]+max; /* Preenche o campo insert da célula t da linha b. */ max= align.bw[b-i][t-j-1].del+PT[b][PDI]; aux1=align.bw[b-i][t-j-1].mat+PT[b][PMI]; aux2=align.bw[b-i][t-j-1].ins+PT[b][PII]; align.bw[b-i][t-j].tb[2]=PDI; if(max<=aux1) max=aux1; align.bw[b-i][t-j].tb[2]=PMI; if(max<aux2) max=aux2; align.bw[b-i][t-j].tb[2]=PII; align.bw[b-i][t-j].ins=iPE[b][IAA(As[t-1])]-f_aa[IAA(As[t-1])]+max; /* Recupera o caminho de Viterbi atraves das seções da matriz de Viterbi para a seqüência. */ dd=0; do teste=tcbk;dd=1; if((teste==PDD || teste==PMD || teste==PID) && bt>i) bt--; dd=0; if(teste==PDD) fprintf(align_out,"D%d(-) ",bt); tcbk=align.bw[bt-i][tt-j].tb[0]; else if(teste==PMD) fprintf(align_out,"M%d(%c) ",bt,As[tt-1]); tcbk=align.bw[bt-i][tt-j].tb[1]; else fprintf(align_out,"I%d(%c) ",bt,As[tt-1]); tcbk=align.bw[bt-i][tt-j].tb[2]; if((teste==PDM || teste==PMM || teste==PIM) && bt>i && tt>j) bt--; tt--; dd=0;
246
if(teste==PDM) fprintf(align_out,"D%d(-) ",bt); tcbk=align.bw[bt-i][tt-j].tb[0]; else if(teste==PMM) fprintf(align_out,"M%d(%c) ",bt,As[tt-1]); tcbk=align.bw[bt-i][tt-j].tb[1];
if(bt==i && tt==j && i>0 && j>0) nn=i; ats=j; ki++; kj++;i=il*(RL-ki); j=ic*(RC-kj);
if(tcbk=PIM) bt--; tt--; fprintf(align_out,"I%d(%c) ",bt,As[tt-1]);
else fprintf(align_out,"I%d(%c) ",bt,As[tt-1]); tcbk=align.bw[bt-i][tt-j].tb[2]; if((teste==PDI || teste==PMI || teste==PII) && tt>j) tt--; dd=0; if(teste==PDI) fprintf(align_out,"D%d(-) ",bt); tcbk=align.bw[bt-i][tt-j].tb[0]; else if(teste==PMI) fprintf(align_out,"M%d(%c) ",bt,As[tt-1]); tcbk=align.bw[bt-i][tt-j].tb[1]; else fprintf(align_out,"I%d(%c) ",bt,As[tt-1]); tcbk=align.bw[bt-i][tt-j].tb[2]; while(dd==0 && tcbk!=9 && tt>1 && bt>1); /* Atualiza as variáveis nn, ats, i, j, ki e kj. */ if(i==0 && j==0) ii++;
else if(bt>i && tt==j && j>0) ats=j; kj++; j=ic*(RC-kj); nn=bt; else if(tt>j && bt==i && i>0) nn=i; ki++; i=il*(RL-ki); ats=tt; else ii++; /* Demais possibilidades --> (bt>i && tt>j && (i>0 || j>0)) */ /* Encerra a fase de retrocedimento para a seqüência. */ if(i==0 && j==0) dd=0; while(dd==0 && tcbk!=9) teste=tcbk;dd=1; if((teste==PDD || teste==PMD || teste==PID) && bt>i) bt--; dd=0; if(teste==PDD) fprintf(align_out,"D%d(-) ",bt); tcbk=align.bw[bt-i][tt-j].tb[0]; else if(teste==PMD) fprintf(align_out,"M%d(%c) ",bt,As[tt-1]); tcbk=align.bw[bt-i][tt-j].tb[1]; else fprintf(align_out,"I%d(%c) ",bt,As[tt-1]); tcbk=align.bw[bt-i][tt-j].tb[2]; if((teste==PDM || teste==PMM || teste==PIM) && bt>i && tt>j) bt--; tt--; dd=0; if(teste==PDM) fprintf(align_out,"D%d(-) ",bt); tcbk=align.bw[bt-i][tt-j].tb[0]; else if(teste==PMM) fprintf(align_out,"M%d(%c) ",bt,As[tt-1]); tcbk=align.bw[bt-i][tt-j].tb[1]; else fprintf(align_out,"I%d(%c) ",bt,As[tt-1]); tcbk=align.bw[bt-i][tt-j].tb[2]; if((teste==PDI || teste==PMI || teste==PII) && tt>j) tt--; dd=0; if(teste==PDI) fprintf(align_out,"D%d(-) ",bt); tcbk=align.bw[bt-i][tt-j].tb[0]; else if(teste==PMI) fprintf(align_out,"M%d(%c) ",bt,As[tt-1]); tcbk=align.bw[bt-i][tt-j].tb[1]; else fprintf(align_out,"I%d(%c) ",bt,As[tt-1]); tcbk=align.bw[bt-i][tt-j].tb[2]; else if(i>0) if(tcbk=PDM) bt--; tt--; fprintf(align_out,"D%d(-) ",bt); else tt--; fprintf(align_out,"D%d(-) ",bt); while(bt>1) bt--; fprintf(align_out,"D%d(-) ",bt); else /* Caso em que j>0. */
else bt--; fprintf(align_out,"I%d(%c) ",bt,As[tt-1]); while(tt>1) tt--; fprintf(align_out,"I%d(%c) ",bt,As[tt-1]); fprintf(align_out,"\n"); /* Efetua a tomada de tempo no final do alinhameto da seqüência. */
247
hpctim(&wtimes, &ctimes, &wbegins, &cbegins); printf("WALL time gasto no alinhamento --> %f\n", wtimes); printf("CPU time gasto no alinhamento --> %f\n", ctimes); fprintf(align_out,"WALL time gasto no alinhamento da seqüência --> %f\n",wtimes); fprintf(align_out,"CPU time gasto no alinhamento da seqüência --> %f\n",ctimes); /* Fechado o arquivo de escrita do escore e do alimhamento. */ if(fclose(align_out)!=0) perror("Erro fechando arquivo de Alinhamento."); exit(0); return(4);
248
GLOSSÁRIO
Aminoácidos: Qualquer uma de uma classe de 20 moléculas que são combinadas para
formar as proteínas. A seqüência de aminoácidos numa proteína é determinada pelo
código genético contido em um gene de uma mólecula de DNA, e desta forma a sua a
sua própria função.
DNA: A molécula que codifica a informação genética. O DNA é uma molécula formada
por dois filamentos que são mantidos juntos por pontes (ligações) fracas entre dos pares
de bases de nucleotídeos. Elas são constituídas por quatro bases de nucleotídeos:
adenina (A), guanina (G), citosina (C) e timina (T). Na natureza os pares de bases
encontrados são formados somente entre A e T, e entre G e C, assim a seqüência de
bases de um simples filamento pode ser deduzido de seu filamento complementar.
Exons: São as seqüências de ADN de um gene que codificam a proteína. Essas
seqüências são separadas pelos introns.
Introns: São as seqüências de ADN que se encontram no meio de um gene,
interrompendo as seqüências de codificação da proteína, que são denominadas exons.
Essas seqüências são transcritas no pré-mARN mas são removidas antes que o mARN
este seja traduzido em proteína.
Nucleotídeos: Uma subunidade do DNA ou do RNA composta de uma base de
nitrogenada, uma molécula fosfato e uma molécula de açucar. As bases nitrogenadas
são adenina, guanina, timina e citosina no DNA e adenina, guanina, uracila e citosina
no RNA. As moléculas de açucar são ribose (RNA) e desoxirribose (DNA). Milhares de
nucleotídeos são ligados para formar uma molécula de DNA ou de RNA.
Resíduo: Cada um dos fragmentos constituintes de um biopolímero, e que mantém, em
grande parte, a estrutura de um dos monômeros que deram origem ao biopolímero.
RNA: Um composto químico encontrado no núcleo e no citoplasma das células, cuja
estrutura química é similar à do DNA. Eles têm um papel importante na síntese das
proteínas e em outras atividades químicas da célula.
249
RNAm: Uma classe de RNA que servem como moldes para a síntese das proteínas. Eles
são transcritos de um gene da seqüência de DNA e o código genético contido nele em
forma de códons pode ser usado para prognosticar a seqüência de aminoácidos na
síntese das proteínas.
RNAt: Uma classe de RNA cujas seqüências formam códons – uma tripla de
nucleotídeos – que são complementares aos códons das seqüências de RNAm. O papel
dos RNAt na síntese das proteínas é se ligarem aos aminoácidos e transferíi-los para os
ribossomos, onde as proteínas são fabricadas (montadas) de acordo com o código
genético contido no RNAm.
250