44
FUNDAÇÃO ESCOLA TÉCNICA LIBERATO SALZANO VIEIRA DA CUNHA CURSO TÉCNICO EM ELETRÔNICA VOX – SISTEMA DE RECONHECIMENTO DE VOZ BASEADO EM REDES NEURAIS FRANCISCO SOCAL LEANDRO MOTTA BARROS RAFAEL DE FIGUEIREDO PROFESSOR ORIENTADOR: DANIEL HART Novo Hamburgo, outubro de 1998.

FUNDAÇÃO ESCOLA TÉCNICA LIBERATO SALZANO VIEIRA … · curso tÉcnico em eletrÔnica vox – sistema de reconhecimento de voz baseado em redes neurais francisco socal leandro motta

  • Upload
    buihanh

  • View
    215

  • Download
    0

Embed Size (px)

Citation preview

FUNDAÇÃO ESCOLA TÉCNICA LIBERATO SALZANO VIEIRA DA CUNHA

CURSO TÉCNICO EM ELETRÔNICA

VOX – SISTEMA DE RECONHECIMENTO DE VOZBASEADO EM REDES NEURAIS

FRANCISCO SOCAL

LEANDRO MOTTA BARROS

RAFAEL DE FIGUEIREDO

PROFESSOR ORIENTADOR: DANIEL HART

Novo Hamburgo, outubro de 1998.

SUMÁRIO

INTRODUÇÃO ................................................................................................4

1.PROJETO DE TRABALHO ..........................................................................5

1.1.Objetivo...................................................................................................5

1.2.Justificativa..............................................................................................5

1.3.Metodologia.............................................................................................6

1.4.Recursos.................................................................................................. 6

1.4.1.Humanos...........................................................................................6

1.4.2.Materiais...........................................................................................7

1.5.Cronograma.............................................................................................7

2.PROCESSAMENTO DA VOZ ......................................................................8

2.1.Características da voz ..............................................................................8

2.1.1.A produção da voz............................................................................8

2.1.2.A composição da voz........................................................................9

2.2.Parametrização da voz .............................................................................9

2.2.1.Análise espectral .............................................................................10

2.2.2.Medida de energia...........................................................................11

3.RECONHECIMENTO.................................................................................12

3.1.Inteligência artificial ..............................................................................12

3

3.2.Redes neurais.........................................................................................13

3.2.1.O neurônio......................................................................................13

3.2.2.Redes feedforward ..........................................................................15

3.2.3.Treinamento....................................................................................16

3.2.4.Projetando uma rede neural .............................................................17

4.IMPLEMENTAÇÃO....................................................................................19

4.1.Aquisição do sinal de voz ......................................................................19

4.1.1.As funções de baixo nível para áudio em forma de onda..................20

4.1.2.A biblioteca de classes para a aquisição de dados............................20

4.2.Processamento dos sinais.......................................................................21

4.2.1.Parametrização................................................................................21

4.2.2.Detecção dos limites das palavras....................................................22

4.2.3.Normalização das amplitudes..........................................................22

4.2.4.Levantamento de dados para o reconhecimento...............................22

4.3.Redes neurais.........................................................................................22

4.4.Programa de teste...................................................................................23

5.RESULTADOS............................................................................................24

CONCLUSÃO ................................................................................................25

REFERÊNCIAS BIBLIOGRÁFICAS.............................................................27

ANEXO 1 – Listagem dos principais arquivos.................................................29

ANEXO 2 – Tela do programa de teste............................................................44

INTRODUÇÃO

O desenvolvimento tecnológico que o mundo vive atualmente é notável. A cada

dia novas tecnologias são desenvolvidas em laboratórios de pesquisa e logo incorpora-

das ao cotidiano. Muitas pessoas, porém, não conseguem acompanhar este desenvolvi-

mento frenético: a quantidade de novidades que surgem é tão grande que elas não são

capazes de se adaptar. Certamente esta situação seria diferente se as formas de interagir

com toda esta tecnologia fossem mais simples. Neste sentido, a possibil idade de coman-

dar máquinas através da voz representa um grande avanço.

Propomo-nos a desenvolver um método de reconhecimento de voz simples, mas

que levante novos questionamentos e apresente novas possibilidades para esta área. Po-

rém, este é um assunto complexo, com muitas variáveis a serem analisadas e otimiza-

das. Para facil itar o desenvolvimento do método ele foi dividido em duas etapas. A pri-

meira consiste em extrair da voz os parâmetros que sejam mais significativos, que a

representem da forma mais eficiente possível. A segunda etapa é responsável pelo reco-

nhecimento propriamente dito.

Este trabalho, que busca relatar o desenvolvimento e implementação do método

criado, está dividido em quatro capítulos. O primeiro é o projeto de trabalho, que repre-

senta nossas expectativas iniciais, antes do início da pesquisa propriamente dita. O se-

gundo e o terceiro capítulos apresentam uma abordagem teórica das duas etapas do tra-

balho. O capítulo final busca mostrar a implementação e os resultados práticos obtidos.

1. PROJETO DE TRABALHO

1.1. Objetivo

Nosso objetivo com este trabalho é pesquisar e desenvolver um método que

permita a um computador “reconhecer” certas palavras quando faladas em um microfo-

ne conectado à placa de som. Mais precisamente, desejamos fazer com que determina-

das ações que normalmente são executas com o mouse ou teclado, possam ser ativadas

através da voz.

Estamos, de fato, mais interessados nos métodos utilizados para fazer isto do que

os resultados efetivamente conseguidos, pois sabemos que estamos trabalhando com

tópicos bastante complexos durante um período relativamente curto.

1.2. Justificativa

Dois argumentos justificam a realização de uma pesquisa nesta área. O primeiro

é o grande número de aplicações para o reconhecimento de fala. Elas vão desde equi-

pamentos voltados para deficientes físicos até sistemas de controle para situações em

que as mãos não podem ou não devem ser utili zadas, como em um rádio de carro. Há

ainda as aplicações voltadas meramente ao conforto, como em um controle-remoto.

O segundo argumento é a possibil idade de preencher uma lacuna que existe em

termos de reconhecimento de fala: são raríssimos os sistemas capazes de oferecer uma

6

boa qualidade de reconhecimento sem necessitar de hardware que vai além das possibi-

lidades dos usuários domésticos.

1.3. Metodologia

Definido o escopo do projeto, o primeiro passo é a realização de uma detalhada

pesquisa, a fim de avaliar cada uma das partes em que o projeto é divido.

A parte inicial é a aquisição dos sinais sonoros através da placa de som do com-

putador. Na seguinte são realizadas transformações matemáticas, com o objetivo de

representar o sinal de uma maneira mais adequada, transformando-o em algo que deno-

minaremos de padrão.

Feita a representação, cabe a outra etapa fazer o reconhecimento propriamente

dito do padrão. É ainda nesta etapa em que define-se quais padrões serão reconhecidos

pelo sistema.

A quarta parte é o gerenciamento dos padrões “aprendidos” , mantendo-os arma-

zenados e possibil itando o acesso de maneira eficiente. A última e óbvia divisão é a

interface gráfica que permite o controle de todos elementos do sistema.

Com as partes definidas e implementadas nos concentraremos em juntá-las e fa-

zer o sistema funcionar. Faremos então os ajustes e calibrações necessários, e acredita-

mos que neste ponto o projeto esteja no nível objetivado inicialmente.

Por se tratar basicamente de uma pesquisa em que visamos desenvolver um mé-

todo e não um produto, possivelmente nos veremos obrigados a alterar o rumo da pes-

quisa, em função de alguma suposição feita inicialmente que não corresponda correta-

mente às nossas expectativas.

1.4. Recursos

1.4.1. Humanos

Para a realização deste trabalho contaremos com o auxílio de alguns professores

da Fundação Liberato. Além do professor Daniel Hart, que nos orienta, sabemos que

7

alguns outros docentes desta escola poderão ajudar na realização deste trabalho. A pro-

fessora Regina Ungaretti presta-nos auxílio no que diz respeito a relatórios e apresenta-

ções. Temos ainda a possibilidade de consultar, através da Internet, pessoas com experi-

ência em diversos assuntos com os quais nos depararemos.

1.4.2. Materiais

Uma vez que se trata de um projeto baseado em software, necessitaremos basi-

camente de computadores para o desenvolvimento dos diversos programas. Como pre-

cisamos ser capazes de gravar sons, estes computadores deverão possuir recursos de

multimídia.

1.5. Cronograma

Maio

Junho

Julho

Agosto

Setem

bro

Outubro

Pesquisa inicial; divisão do trabalho em partesPesquisa aprofundada; início do desenvolvimento de cada parteConclusão de cada parte e sua uniãoEnsaios e ajustes finaisConfecção do relatório e preparação da apresentação

2. PROCESSAMENTO DA VOZ

2.1. Características da voz

A voz humana, sendo analisada como um som qualquer, consiste na variação da

pressão do ar ao longo do tempo. A partir de impulsos elétricos enviados pelo cérebro

humano, o aparelho fonador produz uma seqüência de sons que caracteriza a voz, con-

tendo diversas informações, entre elas a mensagem sendo transmitida. Esta mensagem é

o objeto de estudo de um sistema de reconhecimento de voz, porém é importante que tal

sistema retire as demais informações, como o timbre e o estado emocional do locutor.

2.1.1. A produção da voz

A voz é produzida pela passagem do ar vindo dos pulmões através da laringe,

onde se encontram as cordas vocais. Ao passar pelas cordas, o ar faz com que elas vi-

brem, deixando escapar lufadas de ar que atingem as demais partes do aparelho fonador,

onde a vibração original é modificada. No trato vocal, que compreende a região entre as

cordas vocais e lábios, incluindo as cavidades nasal e oral, são feitas alterações na forma

da onda gerada pelas cordas, dando origem aos diferentes fonemas.

Há, porém, outros sons que compõem a fala: os não vozeados. São caracteriza-

dos por não serem produzidos pela vibração das cordas vocais, mas sim pela liberação

repentina de ar. Como exemplo, tem-se os fonemas /t/ e /s/ da palavra teste.

9

2.1.2. A composição da voz

Uma análise acústica da voz mostra claramente que ela não pode ser considerada

uma onda estacionária, entretanto, suas características permanecem quase constantes

nos diversos segmentos que a compõem. Cada segmento representa um fonema, apre-

sentando características próprias bem definidas, diferenciando-o dos demais.

Fonemas vozeados apresentam uma estrutura harmônica, onde distingue-se cla-

ramente a freqüência fundamental, que é praticamente constante para cada pessoa, além

é claro de suas freqüências harmônicas. Busca-se então identificar a composição fre-

qüencial do fonema, uma vez que a informação desejada está nela contida. Contudo, a

grande variabil idade destes parâmetros em função, não só do locutor, mas de inúmeros

fatores, tem constituído o grande desafio de um sistema de reconhecimento de voz: eli-

minar as variações e chegar a poucos dados que representem claramente um fonema,

independentemente do locutor, do ruído presente e de outros agravantes.

Por outro lado, os fonemas não-vozeados não apresentam esta estrutura harmô-

nica, pois não são formadas pela vibração das cordas vocais. Apresentam de fato com-

ponentes freqüenciais de baixa amplitude distribuídas quase que aleatoriamente ao lon-

go do espectro, como pode-se perceber na figura 2.1.

Figura 2.1 – Espectrograma para a palavra /teste/.

2.2. Parametrização da voz

Para um sistema de reconhecimento de voz, a representação ao longo do tempo

da voz, como é obtida através da digitalização, tem pouco sentido. A variação temporal

10

da amplitude é afetada diretamente por variações no ambiente e no locutor, como é per-

cebido na figura 2.2. Ao ser pronunciado em diferentes situações, o mesmo fonema /a/

apresenta formas de onda sensivelmente diferentes.

Figura 2.2 – O fonema /a/ sendo pronunciado em situações diferentes.

Esta variabil idade, juntamente com o grande volume de dados, inviabil iza a uti-

lização direta da forma de onda no reconhecimento, tornando-se necessário uma correta

parametrização da voz. A parametrização visa basicamente extrair os dados que caracte-

rizem cada fonema, remover redundâncias, ruídos e distorções do sinal.

2.2.1. Análise espectral

A análise em espectro tem como objetivo identificar as freqüências que com-

põem o sinal. A base matemática para esta análise á transformada de Fourier. Para sinais

discretos, representados através de um vetor, utili za-se a DFT (Discrete Fourier Trans-

form, Transformada Discreta de Fourier), dada por:

∑−

=

−=1

0

/2][][N

n

NknjenhkH π

Onde h representa o vetor com os dados temporais, enquanto o vetor resultante

H contém os dados freqüenciais, sendo N o número de amostras.

Existe ainda a FFT (Fast Fourier Transform, Transformada Rápida de Fourier)

que faz uso de métodos computacionais para acelerar a transformação.

11

Como já foi dito anteriormente, a voz caracteriza-se por ser não estacionária, va-

riando suas características freqüenciais ao longo do tempo. Porém em um intervalo sufi-

cientemente curto pode ser considerada como tal. Desta maneira, aplica-se a FFT sobre

as janelas temporais (que podem variar de 10 a 50 mil issegundos) e agrupa-se os vetores

freqüenciais ao longo do tempo formando um espectrograma.

Um espectrograma, como a figura 2.1, indica a variação da amplitude em função

das freqüências e ao longo do tempo. Outra maneira de se desenhar um espectrograma

está exempli ficado na figura 2.3. Neste caso, a amplitude, originalmente indicada pelo

eixo vertical, passa a ser identificada pela tonalidade presente.

Figura 2.3 – Espectrograma para as palavras /abrir/, /fechar/ e/documento/ ditas pausadamente.

Apesar da grande quantidade de dados envolvidos, o espectrograma é um bom

parâmetro para ser reconhecido, uma vez que pode-se identificar nele os fonemas ao

longo do tempo.

2.2.2. Medida de energia

A medida de energia é uma das maneiras mais simples de representar um sinal

de voz, porém não fornece informações suficientes para caracterizar corretamente uma

palavra. Seu uso está ligado à detecção dos limites da palavras. O cálculo da energia é

feito a partir do valor médio quadrático, dado pela equação:

∑−

==

1

0

2 ][1 N

n

nhN

E

Onde h representa o vetor contendo N amostras correspondente à janela temporal

aplicada sobre o sinal de voz.

3. RECONHECIMENTO

As técnicas discutidas até aqui nos permitem extrair de uma palavra falada uma

série de informações que a caracterizam. A primeira idéia que poderia ser pensada para

fazer o reconhecimento propriamente dito seria simples: comparar as informações ex-

traídas de uma palavra falada com as de um banco de dados que contenha as informa-

ções das palavras que deverão ser reconhecidas pelo sistema.

Na prática, porém, esta solução apresenta-se inviável, pois, como uma palavra

nunca é pronunciada da mesma forma, a palavra falada jamais seria encontrada no ban-

co de dados. Problemas como este exigem uma solução mais versátil, capaz de adaptar-

se a todas as variações possíveis na pronúncia. Algoritmos voltados à inteligência artifi-

cial visam exatamente este tipo de problema.

3.1. Inteligência artificial

Os processadores utilizados atualmente são muito diferentes do cérebro humano.

Eles podem pode ser excelentes para a resolução de problemas lógicos ou matemáticos,

mas deixam muito a desejar quando o problema envolve conceitos abstratos. Os estudos

de inteligência artificial buscam dar às máquinas a capacidade de trabalhar de uma for-

ma mais semelhante ao cérebro humano.

Neste sentido, duas técnicas ganharam grande destaque nas duas últimas déca-

das: lógica fuzzy e redes neurais. Ambas buscam inspiração no cérebro humano; a pri-

13

meira procura imitar a forma inexata com que ele percebe as informações enquanto a

segunda, busca inspiração na sua construção física.

Segundo a literatura consultada, redes neurais têm sido utilizadas com grande

sucesso para problemas envolvendo classificação e/ou reconhecimento de padrões.

Como o reconhecimento de voz pode ser considerado como tal, optamos pela utilização

de redes neurais para fazer o reconhecimento.

3.2. Redes neurais

Quando o cérebro humano começou a ser desvendado, descobriu-se que as cé-

lulas que o formam, os neurônios, são elementos muito simples, incapazes de realizar

tarefas complexas. Logo percebeu-se que o cérebro não é um único, grande e poderoso

processador, mas sim um conjunto de bilhões de processadores muito simples traba-

lhando simultaneamente.

Pesquisadores das áreas de informática e eletrônica perceberam que poderiam

utilizar uma estrutura semelhante para criar sistemas com algumas das características do

cérebro. Desta forma, iniciaram-se pesquisas mais detalhadas sobre os neurônios e de

formas de representá-lo matematicamente.

3.2.1. O neurônio

Como já foi comentado, uma rede neural busca inspiração na estrutura do cére-

bro. A unidade básica de nosso cérebro, o neurônio, apresenta uma região onde infor-

mações são processadas (o soma), algumas entradas (os dentritos) e uma saída (o axô-

nio). Os impulsos elétricos recebidos nos dentritos são processados pelo soma e o re-

sultado deste processamento é colocado no axônio.

O modelo de neurônio no qual se baseiam as redes neurais possui uma estrutura

idêntica. Basicamente, a ativação (saída) de um neurônio artificial é uma função da

soma ponderada de suas entradas:

S = f ( E1 * P1 + E2 * P2 + E3 * P3 ) , onde S é a saída, Ex as entradas e Px os pesos

das somas.

14

Figura 3.1 – Esquema de um neurônio artificial

A função f, utili zada para obter a saída do neurônio, é chamada de função de ati-

vação. As funções de ativação mais utilizadas são funções do tipo sigmoidal (com for-

ma de S). A mais utilizada de todas é a função logística: xe

xf −+=

1

1)( .

Figura 3.2 – A função logística

A maior vantagem desta função é sua derivada, facilmente encontrada:

))(1).(()(' xfxfxf −=

A derivada da função de ativação será necessária no processo de treinamento da

rede neural, discutido adiante.

É interessante observar que um único neurônio não é capaz de resolver nenhum

problema prático. Porém, muitos neurônios adequadamente conectados e com os pesos

das conexões devidamente ajustados são capazes de resolver complexos problemas não-

determinísticos. Quanto maior a complexidade do problema a ser resolvido, maior será

o número de neurônios utilizados; para se ter uma idéia, o cérebro humano é formado

15

por cerca de 100 bilhões de neurônios e o número de conexões entre estes neurônios

está na casa das dezenas de trilhões.

3.2.2. Redes feedforward

É possível conectar os neurônios de uma rede neural de modos variados, dando

origem a diversas topologias. A topologia mais utili zada atualmente em problemas prá-

ticos é a feedforward, que pode ser implementada em processadores comuns e, compa-

rando-se com outras topologias, não exige muita memória. Uma rede deste tipo está

representada na figura 3.3.

Figura 3.3 – Rede neural feedforward

Uma rede neural feedforward é composta de algumas camadas. Cada neurônio

de uma camada está conectado a todos os neurônios das camadas adjacentes. É impor-

tante destacar que a camada de entrada, na verdade, não é formada por neurônios reais,

pois eles não realizam nenhum processamento; simplesmente distribuem os valores das

entradas da rede para os neurônios da primeira camada oculta.

Uma rede neural deste tipo, depois de pronta, é capaz de associar uma série de

valores que são colocados em suas entradas a uma determinada saída. Ela não se trata,

porém, simplesmente de uma memória, pois tem a capacidade da generalização; ela

pode encontrar respostas corretas mesmo quando os dados disponíveis para as entradas

estão incompletos ou danificados ou mesmo quando a relação entre entrada e saída não

é concreta. Sabe-se, por exemplo, que há empresas utilizando redes neurais para previ-

são financeira: nas entradas são colocados dados sobre diversos indicadores econômicos

16

e na saída obtém-se informações como a tendência das bolsas valores para o próximo

dia.

O grande problema para a utilização de redes neurais têm sido encontrar regras

que permitam determinar o valor que os pesos das conexões devem ter para que a rede

neural realize a função desejada. O processo pelo qual os pesos de uma rede neural são

determinados é conhecido por treinamento.

3.2.3. Treinamento

O treinamento de redes feedforward é do tipo supervisionado. Neste tipo de trei-

namento é preciso possuir um conjunto de dados para treinamento, ou seja, uma série de

pares de entradas e saídas desejadas. As entradas são apresentadas à rede e seus pesos

são alterados de modo que a saída se aproxime da saída desejada. Pode-se dizer que a

rede neural aprende a fazer seu trabalho observando uma série de exemplos que lhe são

exibidos.

Para alterar os pesos de forma adequada é necessária uma regra. A regra de trei-

namento mais utilizada para o treinamento de redes neurais feedforward é a Error

Backpropagation (retropropagação de erros). A idéia deste algoritmo é atualizar os pe-

sos utilizando as derivadas dos erros em relação aos pesos. O estudo destas derivadas

foi publicado por Rumelhart e McClelland em 1986 e seus resultados estão descritos a

seguir.

Para uma conexão do neurônio j da camada de saída ao neurônio i da camada

oculta anterior, as seguintes equações são válidas:

)).((' jjjj odspf −=δ jiji

oP

E δ.−=∂∂

Onde spj é a soma ponderada que chega ao neurônio j da camada de saída, dj é a

saída desejada para o este mesmo neurônio j, oj é a saída ali obtida e oi é a saída do

neurônio i da camada que antecede a camada de saída.

17

Para pesos que “chegam” às camadas ocultas o cálculo é um pouco mais com-

plexo, pois envolve os “deltas” da próxima camada. Considerando spj a soma ponderada

chegando ao neurônio j da camada oculta em questão, δk os “deltas” da próxima camada

e Pkj o peso do neurônio k da camada anterior ao neurônio j da camada em questão:

∑=k

kjkjj Pspf ).().(' δδ jiji

oP

E δ.−=∂∂

O processo de treinamento é iterativo. Cada vez que um par de “entrada / saída

desejada” é apresentado à rede neural, as derivadas são recalculadas e os pesos são mo-

dificados no sentido inverso desta derivada, de modo a reduzir o peso. Isto é repetido

para todos os exemplos de treinamento, tantas vezes quantas forem necessárias para que

o erro fique dentro de limites aceitáveis. A figura 3.4 mostra a curva típica da redução

do erro durante o treinamento de uma rede neural feedforward. Ela foi obtida a partir do

treinamento de uma rede neural simples.

Figura 3.4 – Treinamento de uma rede neural

3.2.4. Projetando uma rede neural

Criar uma rede neural para a resolução de um problema é uma tarefa que exige

atenção quanto a alguns detalhes. O primeiro deles é a definição da sua forma, quantas

camadas ela deve possuir e quais devem ser seus tamanhos. Teoricamente, qualquer

problema pode ser resolvido por uma rede neural feedforward com duas camadas ocul-

18

tas. Na prática, o mais comum é utili zar apenas uma camada oculta, que é suficiente na

absoluta maioria dos casos.

A determinação do tamanho das camadas de entrada e de saída não é problemá-

tica, já que eles têm uma relação direta com o formato dos dados que utilizaremos nas

entradas e os que desejamos obter nas saídas. Determinar o tamanho da camada oculta

é, segundo diversos autores, um processo de tentativa e erro. Sabe-se que se ela for

muito pequena não terá poder de processamento suficiente para resolver o problema;

por outro lado, se for muito grande, perderá sua capacidade de generalização e atuará

como uma memória.

Também é preciso ter em mente que o conjunto de exemplos utilizados no trei-

namento é crítico. Ele deve conter exemplos que representem o maior número de casos

possíveis, para que o sistema seja capaz de “aprender” a resolver o problema nas mais

diversas situações.

4. IMPLEMENTAÇÃO

O sistema de reconhecimento de voz implementado teve como base a utilização

de microcomputador PC, sendo que a aquisição dos dados foi realizada através de uma

placa de som convencional instalada e configurada para o sistema operacional Win-

dows. Para o desenvolvimento do software, foram utili zados os programas Borland C++

4.52 e Borland C++ Builder.

4.1. Aquisição do sinal de voz

O sistema operacional Windows incorpora em sua API (Application Pro-

gramming Interface, interface de programação de aplicativos) funções para a utilização

dos recursos multimídia de um PC. Basicamente oferece três opções para a gravação de

sons: o uso da MCI (Media Control Interface, interface de controle de mídia) através de

mensagens; o uso da MCI através de strings; e os serviços de baixo nível para audio em

forma de onda.

Cada opção oferece suas vantagens e desvantagens, mas é interessante destacar a

facil idade e simplicidade de uso das duas primeiras opções, porém retornam os dados já

padronizados na forma de um arquivo WAVE, deixando o processo de aquisição extre-

mamente lento. A terceira opção, como o próprio nome já diz, possui a desvantagem de

ser constituída por funções de baixo nível, dificultando a programação, mas por outro

lado, oferecendo a rapidez desejada e os dados gravados diretamente na memória, dei-

xando-os na forma original.

20

Optou-se então pelas funções de baixo nível, tendo em vista a necessidade de

velocidade e dos dados agrupados em porções de memória.

4.1.1. As funções de baixo nível para áudio em forma de onda

A API do Windows fornece uma série de funções para a entrada de áudio em

forma de onda, entre elas: waveI nOpen, waveI nCl ose , waveI nSt art , waveIn S-

to p e waveI nReset responsáveis pelo controle do dispositivo, e as funções wa-

ve I nPr epar eBuf f er , waveI nUnpr epar eBuf f er e waveI nAddBuf f er , responsá-

veis pela manipulação dos blocos de memória a ser preenchidos com os dados (buffers).

Um detalhamento melhor destas pode ser encontrado no Help Online de referência da

API do Windows.

4.1.2. A biblioteca de classes para a aquisição de dados

A utilização destas funções requer um cuidado especial, no que diz respeito à

manipulação da memória. A memória está constantemente sendo atualizada com os va-

lores adquiridos, fazendo com que o programa tenha um controle dinâmico sobre a me-

mória. Optando pela linguagem de programação C++, foi possível desenvolver uma

biblioteca de classes para realizar o encapsulamento das funções da API. A biblioteca

desenvolvida consiste basicamente em duas classes: WaveI n e Recor der . A primeira é

responsável por uma interface mais intuitiva com as funções da API, além de fornecer

um tratamento de erro adequado. A segunda se encarrega de manipular os buffers que

são utilizados para gravar os dados, além de realizar as devidas configurações do dispo-

sitivo de entrada.

A classe WaveI n se encarrega-se de oferecer uma interface orientada à objeto

para a utilização das funções de baixo nível, tendo como variáveis membro um handle

para o dispositivo de entrada e o status do dispositivo. A função Open deve ser utili zada

para abrir um dispositivo de entrada de áudio, fornecendo os parâmetros quanto ao for-

mato desejado dos dados e a janela que receberá as mensagens do dispositivo. Já a fun-

ção Cl ose deve ser utilizada para fechar o dispositivo, enquanto as funções St ar t e

St op são utili zadas no controle de gravação dos dados.

21

Apesar desta funcionalidade, esta classe não deve ser utili zada diretamente, ten-

do seu uso voltado para uma variável membro da classe Recor der , responsável por

uma gama maior de funções.

A classe Recor der possui como membro um objeto do tipo WaveIn , além de

variáveis que contém o formato do som a ser gravado, como o número de amostras por

buffer, de amostras por segundo, de canais e de bits por amostra. Seu uso se dá através

das funções St ar t e St op, que controlam o andamento da gravação propriamente dita,

além da função Fi l l Vect or , que preenche um vetor com o último buffer recebido do

dispositivo.

Um aplicativo que deseja então adquirir áudio em forma de onda deve então,

antes de mais nada, criar um objeto Recor der , especificando os parâmetros desejados

para a digitalização. Para o iniciar o processo, deve acionar a função Start, especifican-

do um handle de uma janela que recebe os dados. Esta janela recebe a mensagem

MM_WI M_DATA, indicando que um buffer está disponível, sendo que é obtido através da

função Fi l l Vect or , fornecida pelo objeto Recor der .

A biblioteca está listada em anexo nos arquivos WAVEIN.H e WAVEIN.CPP,

cabeçalho e código fonte respectivamente.

4.2. Processamento dos sinais

4.2.1. Parametrização

O parâmetro escolhido para o reconhecimento foi a criação de espectrogramas.

Para isso criou-se uma classe para realizar a FFT. No construtor destas classe, são reali-

zados os cálculos iniciais envolvendo os dados necessários para a utili zação do algorit-

mo. Além do construtor, a classe conta também com duas funções Tr ansf orm sobre-

carregadas: uma que aceita como parâmetro um vetor de valores do tipo compl ex , e

outra com valores do tipo doubl e. Esta diferenciação deve-se ao fato do algoritmo da

FFT transformar apenas vetores de números complexos.

O arquivo FFT.H, listado em anexo, contém as definições da classe Fft, en-

quanto o arquivo FFT.CPP contém a classe em si.

22

4.2.2. Detecção dos limites das palavras

Com o intuito de implementar um sistema em tempo real, um algoritmo que de-

tecte os limites das palavras fez-se necessário. O método escolhido foi baseado no cál-

culo da potência do sinal sendo captado. Foi definido assim um limiar de energia, a par-

tir do qual considerou-se uma palavra sendo dita.

4.2.3. Normalização das amplitudes

O sistema apresentou melhora significativa no desempenho quando as amplitu-

des foram normalizadas, antes da aplicação das FFTs. Sem esta normalização, o reco-

nhecimento mostrava-se extremamente dependente de fatores como o volume da voz ou

a posição do microfone.

4.2.4. Levantamento de dados para o reconhecimento

Como já foi dito, os dados escolhidos para realizar o reconhecimento foram os

espectrogramas. Estes foram produzidos através da aplicação da FFT em janelas de 23

ms (equivalente a 256 amostras). Os dados foram amostrados a uma freqüência de

11025 kHz e a uma resolução de 8 bits.

A fim de diminuir o volume de dados a ser analisado, limitou-se as freqüências

do espectrograma em aproximadamente 2 kHz, o que não implica na perda de dados

significativos. Fixou-se também um tamanho de 40 vetores (aproximadamente um se-

gundo) para os espectrogramas analisados.

Pelo fato dos dados espectrais conterem muito ruído (proveniente da digitaliza-

ção e dos resíduos da transformação), buscou-se dar prioridade aos sinais de maior am-

plitude. Para isso elevou-se os dados dos vetores espectrais à diversas potências. A po-

tência 1,5 foi a que melhor se adequou.

4.3. Redes neurais

A implementação das redes neurais feedforward e do algoritmo de treinamento

backpropagation também baseou-se na programação orientada a objetos. Foi criada uma

classe, FeedforwardInputLayer , para representar a camada de entrada e outra,

23

FeedforwardLayer , para representar as camadas de saída e oculta. Estas classes encar-

regam-se de armazenar valores para entradas, saídas, pesos, de calcular os valores das

suas saídas e inicializar os pesos de forma aleatória.

Uma terceira classe, FeedforwardNetwork , representa uma rede neural fe-

edforward de três camadas. Ela possui como membros uma FeedforwardInputLayer e

duas FeedforwardInputLayer , além de funções para obter o valor das saídas para uma

entrada fornecida como argumento, para inicializar todos os pesos aleatoriamente e para

gravar e ler os pesos em arquivos em disco.

Para o treinamento uma outra classe, Backprop , é utili zada. Esta classe exige

como argumento em seu construtor um ponteiro para a FeedforwardNetwork a ser

treinada. Dentre suas funções membro destacamos as funções para inclusão de novos

dados para treinamento, e para a gravação e leitura destes dados em um arquivo. A fun-

ção para o treinamento propriamente dita permite escolher o número máximo de itera-

ções, e um limite mínimo de erro que, quando atingido, pára o treinamento. A função

para treinamento retorna o número de iterações efetivamente feitas e preenche um vetor

com o erro da rede a cada iteração.

As listagens dos arquivos Feedforward.h e Backprop.h, onde estas classes estão

implementadas estão incluídas como anexo deste trabalho.

4.4. Programa de teste

O programa para teste do sistema foi desenvolvido utilizando o compilador

Borland C++ Builder, visando sua utili zação em sistemas operacionais windows de 32

bits, como o Windows 95. Todo o processamento é realizado pelas classes já descritas;

o programa é simplesmente uma interface para o usuário utilizá-las. Uma imagem do

programa foi incluída como anexo.

A camada de entrada da rede neural utilizada tem 1600 neurônios, correspon-

dentes a cada um dos pontos que formam o espectrograma. A camada de saída possui

quatro neurônios, um para cada uma das palavras que pode ser reconhecida. Para a ca-

mada oculta, o valor que mostrou-se mais adequado foi o de 80 neurônios.

5. RESULT ADOS

Os resultados aqui descritos foram obtidos através da utilização do programa

criado para testar o sistema de reconhecimento de voz.

Inicialmente o programa foi treinado para reconhecer as palavras /um/, /dois/,

/três/ e /quatro/. Foram utilizadas 13 amostras de cada palavra, todas ditas pelo mesmo

locutor. O índice de acertos oscilou entre 80% e 85%, porém, é notável a melhora dos

resultados quando o locutor acostuma-se a utili zar o programa e passa a pronunciar as

palavras da maneira mais adequada.

Foi realizado um segundo teste com dados de treinamento compostos pelas pala-

vras /norte/, /sul/, /leste/ e /oeste/ pronunciadas por outro locutor. Nesta situação os

acertos corresponderam a mais de 90% dos casos. A razão desta sensível melhora foi a

utilização de palavras mais extensas, aumentando assim a diferença entre elas.

Um terceiro experimento foi feito treinando a rede neural com três locutores. As

palavras utilizadas foram /abrir/, /fechar/, /editar/ e /inserir/, pronunciadas 5 vezes por

cada um. Neste caso percebeu-se um decaimento da eficiência, abaixando a taxa de re-

conhecimento para próximo de 60%, variando até 70% em função do locutor e da pala-

vra em questão.

CONCLUSÃO

O reconhecimento de voz é um assunto complexo. Existem inúmeras variáveis a

serem otimizadas, decisões a serem tomadas e detalhes a serem analisados. O desenvol-

vimento de uma pesquisa nesta área exige muito trabalho. Acreditamos os resultados

obtidos foram muito bons, considerando a complexidade da proposta e o tempo dedica-

do a ela.

O sistema que desenvolvemos mostrou-se capaz de reconhecer com considerável

precisão palavras isoladas de um vocabulário bastante limitado. Certamente tal sistema

não é o ideal para ser utili zado com interface entre o homem e a máquina. É, porém, um

excelente ponto de partida a partir do qual um sistema mais complexo possa ser desen-

volvido. Acreditamos que a maior contribuição desta pesquisa não é a criação de um

novo método para reconhecimento de voz, mas sim a avaliação sobre a possibil idade da

utilização de novas técnicas.

Redes neurais, por exemplo, têm sido pouco exploradas nesta área. Segundo al-

guns especialistas este é o futuro do reconhecimento de voz. Pelos testes que fizemos

não temos como negar esta afirmação, pois mesmo uma rede neural simples como a que

foi utilizada mostrou-se eficiente.

Também foi possível averiguar se os espectrogramas seriam uma forma adequa-

da de representar a voz para o reconhecimento. Verificamos que um espectrograma tal e

26

qual é criado a partir de um som em forma de onda não é o ideal. Foi necessário alterar

o espectrograma para que o sistema apresentasse melhor desempenho. Ficou claro que

um espectrograma possui todas as informações necessárias para identificar palavras e

fonemas, mas a parametrização precisa ser levada adiante.

É possível melhorar as duas etapas principais do sistema a fim de melhorar seu

desempenho. Seria possível, por exemplo, fazer uma análise matemática das harmônicas

dos fonemas para identificá-los mais precisamente. É preciso também buscar formas de

diferenciar fonemas não-vozeados, que mostraram-se os de mais difícil identificação.

Quanto à etapa de reconhecimento, seria possível utilizar uma rede neural que se adap-

tasse ao locutor, de modo que a eficiência do sistema fosse sendo incrementada auto-

maticamente durante seu uso. Redes neurais com esta característica são mais comple-

xas, mas perfeitamente possíveis.

Sabemos também que o sistema carece de recursos que permitam ignorar deter-

minados fatores. O tipo de microfone utilizado, por exemplo, influencia nos resultados

de forma relevante.

É possível que no futuro o comando de máquinas pela voz faça com que mais

pessoas tenham acesso às tecnologias que são criadas para melhorar a nossa vida. Espe-

ramos com este trabalho estar contribuindo para que isto se torne realidade.

REFERÊNCIAS BIBLIOGRÁFICAS

1. CONNOR, F. R. Tópicos de Introdução à Electrónica e às Telecomunicações –

Sinais. Lisboa, Interciência, 1978. 93 p.

2. JAIN, Anil K. Fundamentals of Digital Image Processing. Engelwood Hill s,

Prentice Hall , 1989. 569 p.

3. KOVÁCS, Zsolt. Redes Neurais Artificiais. Teoria e Aplicação. São Paulo, co-

llegium cognitio, 1996. 139 p.

4. LIM, Jae S. Two-Dimensional Signal and Image Processing. Engelwood Hill s,

Prentice Hall , 1990. 694 p.

5. LUFT, Joel. Reconhecimento Automático de Voz para Palavras Isoladas e Inde-

pendente do Locutor. Dissertação de mestrado, PPGEMM, Universidade

Federal do Rio Grande do Sul, 1994.

6. MARKOWITZ, Judith A. Using Speech Recognition. New Jersey, Prentice Hall,

1996. 292 p.

7. MASTERS, Timothy. Practical Neural Networks Recipes in C++ . San Diego,

Academic Press, 1993. 493 p.

8. Microsoft Multimedia Programmer’s Reference. Microsoft Corporation, 1996.

28

9. MILANO, John; CABANSKY, Tom & HOWE, Harold. Borland C++ Builder

How To. Corte Madera, Waite Group Press, 1997. 822 p.-

10. NORTON, Peter & YAO, Paul. Programando em Borland C++ para Windows.

São Paulo, Ed. Berkley, 1992. 584 p.

11. OKANO, Emico; CALDAS, Iberê L. & CHOW, Ceci. Física Para Ciências Bi-

omédicas. São Paulo, Harbra, 1982. 612 p.

12. ORTH, A. Reconhecimento Automático de Peças. Revista Saber Eletrônica. Ano

34. No 308. São Paulo, Outubro 1998.

13. OHSMANN, M. Introduction to Digital Signal Processing. Elektor Electronics.

No 262. Janeiro de 1998.

14. RAO, Valluru B. & RAO, Hayagriva V. C++ Neural Networks & Fuzzy Logic.

New York, MIS:Press, 1995. 551 p.

15. Reliable Software Web Site. www.relisoft.com

ANEXO 1 – LISTAGEM DOS PRINCIPAIS ARQUIVOS

Wavein.h

#ifndef _WAVEIN_H#define _WAVEIN_H

#include <windows.h>#include "svector.h"

#pragma warn -sig

class WaveHeader : public WAVEHDR{public:

bool isDone() const { return dwFlags & WHDR_DONE ; } ;} ;

class WaveFormat : public WAVEFORMATEX{public:

WaveFormat( DWORD rate, WORD chan, WORD bits ){

wFormatTag = WAVE_FORMAT_PCM ;nChannels = chan ;nSamplesPerSec = rate ;nAvgBytesPerSec = chan * rate * bits / 8 ;nBlockAlign = chan * bits / 8 ;wBitsPerSample = bits ;cbSize = 0 ;

} ;} ;

class WaveIn{public:

WaveIn() : status( MMSYSERR_BADDEVICEID ) {} ;~WaveIn() { if( ok() ) { Stop(); Reset() ; Close() ; } ; } ;

bool Open( HWND, UINT, WaveFormat& ) ;bool Close() ;void Reset() { if( ok() ) waveInReset( hWave ) ; } ;

void Start() { waveInStart( hWave ) ; } ;

30

void Stop() { waveInStop( hWave ) ; } ;

void Prepare( WaveHea der* ) ;void UnPrepare( WaveHeader* ) ;void Send( WaveHeader* ) ;

LPSTR queryError() ;LPCSTR queryTitle() { return "WaveAudio Input Engine" ; } ;bool ok() { return status == 0 ; } ;bool isInUse() { return status == MMSYSERR_ALLOCATED ; } ;

private:

HWAVEIN hWave ;UINT status ;char errorText[164] ;

} ;

class Recorder{

enum { NUM_BUF = 8 } ;public:

Recorder( WORD cSamples, DWORD cSamplesPerSec, WORD nChannels, WORD bits ) ;~Recorder() ;

bool Start( HWND hwnd ) ;void Stop() ;

bool isBufferDone() const { return _header[_iBuf].isDone() ; } ;bool BufferDone() ;

bool FillVector( svector<double>& ) ;

WORD SampleCount() const { return _cSamples ; } ;DWORD SamplesPerSec() const { return _cSamplesPerSec ; } ;WORD Bits() const { return _bits ; } ;WORD Channels() const { return _nChannels ; } ;

protected:

WaveIn _wave ;int _iBuf ;

WORD _cSamples ;DWORD _cSamplesPerSec ;WORD _nChannels ;WORD _bits ;WORD _cbBuf ;

WaveHeader _header [ NUM_BUF ] ;LPSTR _dataPool ;svector<int> avoid_errors ;

} ;

inline bool WaveIn::Open( HWND hWnd, UINT id, WaveFormat& fmt ){

status = waveInOpen( &hWave, id, &fmt, (DWORD)hWnd, NULL, CALLBACK_WINDOW ) ;return ok() ;

} ;

inline bool WaveIn::Close(){

if( waveInClose( hWave ) == 0 && ok() ){

status = MMSYSERR_BADDEVICEID ;return true ;

} ;return false ;

} ;

inline void WaveIn::Prepare( WaveHeader* phdr ){

31

waveInPrepareHeader( hWave, phdr, sizeof(WAVEHDR) ) ;} ;

inline void WaveIn::UnPrepare( WaveHeader* phdr ){

waveInUnprepareHeader( hWave, phdr, sizeof(WAVEHDR) ) ;} ;

inline void WaveIn::Send( WaveHeader* phdr ){

waveInAddBuffer( hWave, phdr, sizeof(WAVEHDR) ) ;} ;

inline LPSTR WaveIn::queryError(){

waveInGetErrorText( status, errorText, sizeof(errorTex t) ) ;return errorText ;

} ;#endif

Wavein.cpp

#ifndef wavein_cpp#define wavein_cpp

#pragma warn -sig#include "wavein.h"

Recorder::Recorder( WORD cSamples, DWORD cSamplesPerSec, WORD nChannels, WORD bits ): _cSamples( cSamples ),

_cSamplesPerSec ( cSamplesPerSec ),_nChannels( nChannels ),_bits( bits ),_iBuf(0),_cbBuf( cSamples * nChannels * bits / 8 )

{_dataPool = new char[ _cbBuf*NUM_BUF ] ;assert( _dataPool != NULL ) ;

} ;

Recorder::~Recorder(){

Stop() ;delete [] _dataPool ;

} ;

bool Recorder::Start( HWND hwnd ){

LPSTR errorText ;

WaveFormat format( _cSamplesPerSec, _nChannels, _bits ) ;_wave.Open( hwnd, 0, format ) ;

if( !_wave.ok() ){

if( _wave.isInUse() )errorText = "Outro aplicativo está utilizando o disposit ivo de entrada." ;

elseerrorText = _wave.queryError() ;

MessageBox( hwnd, errorText, _wave.queryTitle(), MB_ICONSTOP ) ;return false ;

} ;

for( int i=0; i<NUM_BUF - 1; i++ ){

_header[i].lpData = &_dataPool[ i*_cbBuf ] ;_header[i].dwBu fferLength = _cbBuf ;

32

_header[i].dwFlags = 0 ;_header[i].dwLoops = 0 ;

_wave.Prepare( &_header[i] ) ;_wave.Send( &_header[i] ) ;

} ;

_iBuf = 0 ;_wave.Start() ;return true ;

} ;

void Recorder::Stop(){

_wave.Reset() ;_wave.Close() ;

} ;

bool Recorder::BufferDone(){

if( !_wave.ok() ) return false ;

assert( isBufferDone() ) ;

_wave.UnPrepare( &_header[ _iBuf ] ) ;

int prevBuf = _iBuf - 1 ;if( prevBuf < 0 ) prevBuf = NUM_BUF - 1 ;

_iBuf++ ;if( _iBuf == NUM_BUF ) _iBuf = 0 ;

_header[prevBuf].lpData = &_dataPool[ prevBuf*_cbBuf ] ;_header[prevBuf].dwBufferLength = _cbBuf ;_header[prevBuf].dwFlags = 0 ;_header[prevBuf].dwLoops = 0 ;

_wave.Prepare( &_header[prevBuf] ) ;_wave.Send( &_header[prevBuf] ) ;

return true ;} ;

bool Recorder::FillVector( svector<double>& vc ){ #pragma warn -csu

if( !_wave.ok() ) return false ;

assert( isBufferDone() ) ;

int i ;WaveHeader* hdr = &_header[_iBuf] ;LPSTR data = hdr->lpData ;

vc.resize( hdr->dwBytesRecorded / (_nC hannels*_bits/8) ) ;

if( _bits == 8 && _nChannels == 1 ){

for( i=0 ; i<vc.limit() ; i++ )vc[i] = (((unsigned char)data[i]-128)) ;

}else if( _bits == 16 && _nChannels == 1 ){

for( i=0 ; i<vc.limit() ; i++ )vc[i] = (((short*)data)[i]) / 64 ;

}else return false ;return true ;

#pragma warn +csu } ;

#endif

33

Fft.h

#ifndef _FFT_H#define _FFT_H

#include <stdlib.h>#include <complex.h>

#include "svector.h"

class WrongNumberOfPoints{} ;

inline double abs( double vl ){

return fab s( vl ) ;} ;

inline double round ( double vl ){

return (vl-floor(vl)) < 0.5 ? floor(vl) : ceil(vl) ;} ;

class Fft{public:

Fft( int Points ) ;~Fft() {} ;int Points () const { return _Points ; } ;

svector<complex>& Transform( svector<complex>& ) ;svector<double>& Transform( svector<double>& ) ;

protected:void _Transform() ;int _Points ;const double pi ;

private :int _logPoints ;double _sqrtPoints ;svector<int> _aBitRev ;svector< svector< complex > > _W ;svector<complex> x ;

svector<double> vec_char ; // Devido a um bug do compilador ao trabalhar} ; // com Templates de classes sem nenhum objeto criado

#endif

Fft.cpp

#ifndef fft_cpp#define fft_cpp

#pragma warn -sig#pragma warn -csu

#include "fft.h"#include <math.h>

Fft::Fft( int Points ) : _Points( Points ),_aBitRev( _Points ), pi( M_PI ), x( _Points )

{_sqrtPoints = sqrt( (double) _Points ) ;

_logPoints = 0 ;Points-- ;

34

while( Points != 0 ){

Points >>= 1 ;_logPoints++ ;

} ;

_W.resize( _l ogPoints + 1 ) ;

int _2_l = 2 ; double re, im ;

for( int l=1 ; l < _W.limit() ; l++ ){

_W[l].resize( _Points ) ;

for( int i=0; i < _W[l].limit() ; i++ ){

re = cos( 2.0 * pi * i / _2_l ) ;im = -sin( 2.0 * pi * i / _2_l ) ;_W[l][ i] = complex( re, im ) ;

} ;_2_l <<= 1 ;

} ;

int rev = 0 ;int halfPoints = _Points >> 1 ;

int mask ;

for( int j=0 ; j < _Points - 1 ; j++ ){

x[j] = complex( 0, 0 ) ;

_aBitRev[j] = rev ;mask = halfPoints ;

while( rev >= mask ){

rev -= mask ;mask >>= 1 ;

} ;rev += mask ;

} ;_aBitRev[ _Points-1 ] = _Points - 1 ;

} ;

svector<complex>& Fft::Transform( svector<complex>& vc ){

int i ;

if( vc.limit() != _Points )throw WrongNumberOfPoints() ;

for( i=0; i<_Point s ; i++ )x[ _aBitRev[i] ] = vc[i] ;

_Transform() ;

vc = x ;

return vc ;} ;

svector<double>& Fft::Transform( svector<double>& vc ){

int i ;

if( vc.limit() != _Points )throw WrongNumberOfPoints() ;

for( i=0; i<_Points ; i++ )x[ _aBitRe v[i] ] = complex( vc[i], 0 ) ;

35

_Transform() ;

vc.resize( _Points / 2 ) ;for( i=0; i<vc.limit() ; i++ )

vc[i] = abs( x[1+i] ) / _Points ;

return vc ;} ;

void Fft::_Transform(){

int step = 1 ;int increm ;int i, j ;complex u, t ;

for( i nt level=1; level < _W.limit(); level++ ){

increm = step*2 ;for( j=0; j<step; j++ ){

u = _W [level] [j] ;for( i=j; i<_Points ; i += increm ){

t = u ;t *= x [i+step] ;x [i+step] = x[i] ;x [i+step] -= t ;x [i] += t ;

} ;} ;step <<= 1 ;

} ;} ;

#endif

Feedforward.h

#ifndef Feedforward_h#define Feedforward_h

#include <math.h>

inline float RandomWeight(){ float Num; Num = rand() % 1000; return 2 * (Num / 1000) – 1;}

float DotProd (int sz, float *vet1, float *vet2){

int k, m, p = 0; float sum = 0; k = sz / 4; m = sz % 4;

while (k--) { sum += vet1[p] * vet2[p++]; // Fazendo desta forma, de 4 em 4, o número sum += vet1[p] * vet2[p++]; // de loops é reduzido. Isto deve acelerar sum += vet1[p] * vet2[p++]; // bastante o processo, especialmente se o sum += vet1[p] * vet2[p++]; // processador utilizado usar pipelining. }

36

while (m--) sum += vet1[p] * vet2[p++];

return sum;}

#define F_TABLE_LENGTH 100#define F_TABLE_MAX 10.0static float f_factor, f_f[F_TABLE_LENGTH], f_d[F_TABLE_LENGTH];

void InitActFunc(){ f_factor = (float)(F_TABLE_LENGTH - 1) / (float)F_TABLE_MAX; for (int c = 0 ; c < F_TABLE_LENGTH ; c++) { f_f[c] = 1.0 / (1.0 + exp (-((float)c) / f_factor)); if (c) f_d[c-1] = f_f[c] - f_f[c-1]; }}

float ActFunc (float x){ int i; float xd; // distancia no eixo x

if (x >= 0) { xd = x * f_factor; i = (int)xd; if (i >= (F_TABLE_LENGTH - 1)) return f_f[F_TABLE_LENGTH - 1]; return f_f[i] + f_d[i] * (xd - i); } else { xd = -x * f_factor; // x é negativo, logo, xd é positivo (f_factor é positivo) i = (int)xd; if (i >= (F_TABLE_LENGTH - 1)) return 1.0 - f_f[F_TABLE_LENGTH - 1]; return 1.0 - f_f[i] + f_d[i] * (xd - i); }}

float ActDeriv(float f) // O parâmetro é f(x), não simplesmente x{ return f * (1.0 - f);}

// ============================================================================// = FeedforwardInputLayer =// ============================================================================

class FeedforwardInputLayer{ public: FeedforwardInputLayer(int sz); ~FeedforwardInputLayer(); float *Outputs; int NumNrns;};

// - Construtor ---------------------------------------------------------------FeedforwardInputLayer::FeedforwardInputLayer(int sz){ Outputs = new float[sz]; NumNrns = sz;}

// - Destrutor ----------------------------------------------------------------FeedforwardInputLayer::~FeedforwardInputLayer(){ delete[] Outputs;}

37

// ============================================================================// = FeedforwardLayer =// ============================================================================

class FeedforwardLayer{ public: FeedforwardLayer(int szThis, int szPrev); ~FeedforwardLayer(); void ForwardPropagate(); void RandomizeWeights(); float *Inputs; float *Outputs; float *Weights; int NumNrns; int NumNrnsPrev;};

// - Construtor ---------------------------------------------------------------FeedforwardLayer::FeedforwardLayer(int szThis, int szPrev){ Outputs = new float[szThis]; Weights = new float[szThis * szPrev]; NumNrns = szThis; NumNrnsPrev = szPrev;}

// - Destrutor ----------------------------------------------------------------FeedforwardLayer::~FeedforwardLayer(){ delete[] Outputs; delete[] Weights;}

// - ForwardPropagate ---------------------------------------------------------void FeedforwardLayer::ForwardPropagate(){ float *vcWeights;

vcWeights = new float[NumNrnsPrev];

for (int eON = 0 ; eON < NumNrns ; eON++) // Para cada neurônio da camada { for (int eIN = 0 ; eIN < NumNrnsPrev ; eIN++) vcWeights[eIN] = Weights[NumNrnsPrev * eON + eIN];

Outputs[eON] = ActFunc(DotProd(NumNrnsPrev, Inputs, vcWeights)); }

delete[] vcWeights;}

// - RandomizeWeights ---------------------------------------------------------void FeedforwardLayer::RandomizeWeights(){ for (int c = 0 ; c < NumNrns * NumNrnsPrev ; c++) Weights[c] = RandomWeight();}

typedef FeedforwardLayer FeedforwardOutputLayer;

typedef FeedforwardLayer FeedforwardHiddenLayer;

// ============================================================================// = FeedforwardNetwork =// ============================================================================

class FeedforwardNetwork{ friend class Backprop; public: FeedforwardNetwork(int szInp, int szHid, int szOut);

38

~FeedforwardNetwork(); float *GetOutputs(float *Inputs); void RandomizeWeights(); void WriteWeights (char *FileName); bool ReadWeights (char *FileName); // retorna true se OK FeedforwardInputLayer *InputLayer; FeedforwardHiddenLayer *HiddenLayer; FeedforwardOutputLayer *OutputLayer;};

// - Construtor ---------------------------------------------------------------FeedforwardNetwork::FeedforwardNetwork(int szInp, int szHid, int szOut){ InputLayer = new FeedforwardInputLayer(szInp); HiddenLayer = new FeedforwardHiddenLayer(szHid, szInp); OutputLayer = new FeedforwardOutputLayer(szOut, szHid); HiddenLayer->Inputs = InputLayer->Outputs; OutputLayer->Inputs = HiddenLayer->Outputs; InitActFunc();}

// - Destrutor ----------------------------------------------------------------FeedforwardNetwork::~FeedforwardNetwork(){ delete InputLayer; delete HiddenLayer; delete OutputLayer;}

// - GetOutputs ---------------------------------------------------------------float *FeedforwardNetwork::GetOutputs(float *Inputs){ for (int c = 0 ; c < InputLayer->NumNrns ; c++) InputLayer->Outputs[c] = Inputs[c]; HiddenLayer->ForwardPropagate(); OutputLayer->ForwardPropagate(); return OutputLayer->Outputs;}

// - RandomizeWeights ---------------------------------------------------------void FeedforwardNetwork::RandomizeWeights(){ HiddenLayer->RandomizeWeights(); OutputLayer->RandomizeWeights();}

// - WriteWeights ------------------------------------------------------------void FeedforwardNetwork::WriteWeights (char* FileName){ TFileStream *File; unsigned short int *iBuf; float *fBuf; unsigned long int Pos = 0;

File = new TFileStream (FileName, fmCreate);

iBuf = new unsigned short int [3]; iBuf[0] = (unsigned short int)InputLayer->NumNrns; iBuf[1] = (unsigned short int)HiddenLayer->NumNrns; iBuf[2] = (unsigned short int)OutputLayer->NumNrns;

File->Write((void*)iBuf, 6);

fBuf = new float [ (iBuf[0] * iBuf[1]) + (iBuf[1] * iBuf[2]) ];

for (int eHN = 0 ; eHN < HiddenLayer->NumNrns ; eHN++) for (int eIN = 0 ; eIN < InputLayer->NumNrns ; eIN++) fBuf[Pos++] = HiddenLayer->Weights[InputLayer->NumNrns * eHN + eIN];

for (int eON = 0 ; eON < OutputLayer->NumNrns ; eON++) for (int eHN = 0 ; eHN < HiddenLayer->NumNrns ; eHN++) fBuf[Pos++] = OutputLayer->Weights[HiddenLayer->NumNrns * eON + eHN];

39

File->Write( (void*)fBuf, ( (iBuf[0] * iBuf[1]) + (iBuf[1] * iBuf[2])) * 4);

delete[] fBuf; delete[] iBuf; delete File;}

// - ReadWeights -------------------------------------------------------------bool FeedforwardNetwork::ReadWeights (char* FileName){ TFileStream *File; unsigned short int *iBuf; float *fBuf; unsigned long int Pos = 0;

File = new TFileStream (FileName, fmOpenRead);

iBuf = new unsigned short int [3]; File->Read((void*)iBuf, 6); // São 3 unsigned short int's -> 6 bytes

if ( iBuf[0] != InputLayer->NumNrns || iBuf[1] != HiddenLayer->NumNrns || iBuf[2] != OutputLayer->NumNrns ) { delete[] iBuf; return false; }

fBuf = new float [ (iBuf[0] * iBuf[1]) + (iBuf[1] * iBuf[2]) ];

File->Read((void*)fBuf, ((iBuf[0] * iBuf[1]) + (iBuf[1] * iBuf[2])) * 4);

for (int eHN = 0 ; eHN < iBuf[1] ; eHN++) for (int eIN = 0 ; eIN < iBuf[0] ; eIN++) HiddenLayer->Weights[iBuf[0] * eHN + eIN] = fBuf[Pos++];

for (int eON = 0 ; eON < iBuf[2] ; eON++) for (int eHN = 0 ; eHN < iBuf[1] ; eHN++) OutputLayer->Weights[iBuf[1] * eON + eHN] = fBuf[Pos++];

delete[] fBuf; delete[] iBuf; delete File;

return true;}

#endif

Backprop.h

#ifndef Backprop_h#define Backprop_h

#include <Feedforward.h>

// ============================================================================// = Backprop - Faz o treinamento propriamente dito =// ============================================================================

class Backprop{ public: Backprop(FeedforwardNetwork *pNet); ~Backprop(); float GetInput(int Pair, int Nrn);

40

float GetOutput(int Pair, int Nrn); void AddPair(float *Input, float *Output); float GetError(float *Targets); // Retorna o erro médio quadrático (MSE) float GetEpochError(); // Retorna MSE para todos os pares void CalcGrads(int Pair); // Calcula gradientes para o par argumentado int Train(int MaxIter, float ErrTol, float LR, float *ErrVect); // Treina bool ReadData(char *FileName); // retorna true se OK void WriteData(char *FileName); FeedforwardNetwork *Net; // Ponteiro para a rede a ser treinada float *Data; // Dados (entrada/saida desejada) float *GradsH, *GradsO; // Gradientes da camada oculta / saida int NumPairs, InputSize, HiddenSize, OutputSize; bool HasAPair;};

// - Construtor ---------------------------------------------------------------Backprop::Backprop(FeedforwardNetwork *pNet){ NumPairs = 0; HasAPair = false; Net = pNet; InputSize = Net->InputLayer->NumNrns; HiddenSize = Net->HiddenLayer->NumNrns; OutputSize = Net->OutputLayer->NumNrns; GradsH = new float[HiddenSize * InputSize]; GradsO = new float[OutputSize * HiddenSize];}

// - Destrutor ----------------------------------------------------------------Backprop::~Backprop(){ if (HasAPair) delete[] Data; delete[] GradsH; delete[] GradsO;}

// - GetInput -----------------------------------------------------------------float Backprop::GetInput(int Pair, int Nrn){ return Data[(InputSize+OutputSize) * Pair + Nrn];}

// - GetOutput ----------------------------------------------------------------float Backprop::GetOutput(int Pair, int Nrn){ return Data[(InputSize+OutputSize) * Pair + InputSize + Nrn];}

// - AddPair ------------------------------------------------------------------void Backprop::AddPair(float *Input, float *Output){ float *vcTmp; int Pos = 0; int PairSize = InputSize + OutputSize;

vcTmp = new float[PairSize * NumPairs];

for (int c = 0 ; c < PairSize * NumPairs ; c++) vcTmp[c] = Data[c];

if (HasAPair) delete[] Data; Data = new float[PairSize * (NumPairs + 1)];

for (int c = 0 ; c < PairSize * NumPairs ; c++) Data[c] = vcTmp[c];

for (int c = PairSize * NumPairs ; c < PairSize * NumPairs + InputSize; c++) Data[c] = Input[Pos++];

Pos = 0; for (int c = PairSize * NumPairs + InputSize; c < PairSize * (NumPairs + 1) ; c++) Data[c] = Output[Pos++];

41

NumPairs++; HasAPair = true;

delete[] vcTmp;}

// - GetError -----------------------------------------------------------------float Backprop::GetError(float *Targets){ float Err, Sum = 0;

for (int c = 0 ; c < OutputSize ; c++) { Err = Targets[c] - Net->OutputLayer->Outputs[c]; Sum += Err * Err; } return Sum / (float)OutputSize;}

// - GetEpochError ------------------------------------------------------------float Backprop::GetEpochError(){ float AccErr = 0, *Inp, *Trgt;

Inp = new float[InputSize]; Trgt = new float[OutputSize];

for (int eP = 0 ; eP < NumPairs ; eP++) { for (int eI = 0 ; eI < InputSize ; eI++) Inp[eI] = GetInput(eP, eI);

for (int eO = 0 ; eO < OutputSize ; eO++) Trgt[eO] = GetOutput(eP, eO);

Net->GetOutputs(Inp);

AccErr += GetError(Trgt); }

AccErr /= (float)NumPairs;

delete[] Inp; delete[] Trgt;

return AccErr;}

// - CalcGrads ----------------------------------------------------------------void Backprop::CalcGrads(int Pair){ float Out, Delta, Sum, *Inp, *Trgt, *Deltas;

Deltas = new float[OutputSize]; Inp = new float[InputSize]; Trgt = new float[OutputSize];

for (int eI = 0 ; eI < InputSize ; eI++) Inp[eI] = GetInput(Pair, eI);

for (int eO = 0 ; eO < OutputSize ; eO++) Trgt[eO] = GetOutput(Pair, eO);

Net->GetOutputs(Inp);

for (int eON = 0 ; eON < OutputSize ; eON++) { Out = Net->OutputLayer->Outputs[eON]; Delta = (Trgt[eON] - Out) * ActDeriv(Out); Deltas[eON] = Delta; for (int eHN = 0 ; eHN < HiddenSize ; eHN++)

42

GradsO[HiddenSize * eON + eHN] = Delta * Net->HiddenLayer->Outputs[eHN]; }

for (int eHN = 0 ; eHN < HiddenSize ; eHN++) { Sum = 0; for (int eON = 0 ; eON < OutputSize ; eON++) Sum += Deltas[eON] * Net->OutputLayer->Weights[eON * HiddenSize + eHN];

Delta = Sum * ActDeriv(Net->HiddenLayer->Outputs[eHN]);

for (int eIN = 0 ; eIN < InputSize ; eIN++) GradsH[InputSize * eHN + eIN] = Delta * Net->InputLayer->Outputs[eIN]; }

delete[] Deltas; delete[] Inp; delete[] Trgt;}

// - Train --------------------------------------------------------------------int Backprop::Train (int MaxIter, float ErrTol, float LR, float *ErrVect){ float Corr, Err;

for (int Iter = 0 ; Iter < MaxIter ; Iter++) { Err = GetEpochError();

if (Err < ErrTol) { for (int c = Iter ; c < MaxIter ; c++) ErrVect[c] = Err; return Iter + 1; }

ErrVect[Iter] = Err;

for (int eP = 0 ; eP < NumPairs ; eP++) { CalcGrads(eP);

for (int eOW = 0 ; eOW < OutputSize * HiddenSize ; eOW++) { Corr = LR * GradsO[eOW]; Net->OutputLayer->Weights[eOW] += Corr; }

for (int eHW = 0 ; eHW < HiddenSize * InputSize ; eHW++) { Corr = LR * GradsH[eHW]; Net->HiddenLayer->Weights[eHW] += Corr; } } } return MaxIter;}

// - WriteData ----------------------------------------------------------------void Backprop::WriteData(char *FileName){ TFileStream *File; unsigned short int *iBuf; float *fBuf;

File = new TFileStream (FileName, fmCreate);

iBuf = new unsigned short int [3]; iBuf[0] = (unsigned short int)NumPairs; iBuf[1] = (unsigned short int)InputSize; iBuf[2] = (unsigned short int)OutputSize; File->Write((void*)iBuf, 6);

43

fBuf = new float [(InputSize + OutputSize) * NumPairs];

for (unsigned short int ePair = 0 ; ePair < NumPairs ; ePair++ ) { for (unsigned short int eInp = 0 ; eInp < InputSize ; eInp++ ) fBuf[(ePair * (InputSize + OutputSize)) + eInp] = GetInput(ePair, eInp); for (unsigned short int eOut = 0 ; eOut < OutputSize ; eOut++ ) fBuf[(ePair * (InputSize + OutputSize)) + InputSize + eOut] = GetOutput(ePair, eOut); }

File->Write((void*)fBuf, (InputSize + OutputSize) * NumPairs * 4);

delete[] fBuf; delete[] iBuf; delete File;

}

// -- ReadData ----------------------------------------------------------------bool Backprop::ReadData (char *FileName){ TFileStream *File; unsigned short int *iBuf; float *fBuf, *NewInput, *NewOutput; int PairSize, NewNumPairs;

File = new TFileStream (FileName, fmOpenRead);

iBuf = new unsigned short int [3]; File->Read((void*)iBuf, 6);

if (iBuf[1] != InputSize && iBuf[2] != OutputSize) return false;

NewInput = new float[InputSize]; NewOutput = new float[OutputSize];

NewNumPairs = iBuf[0]; PairSize = InputSize + OutputSize; if (HasAPair) delete[] Data; Data = new float[PairSize * NewNumPairs];

fBuf = new float [PairSize * NewNumPairs];

File->Read((void*)fBuf, PairSize * NewNumPairs * 4);

for (int ePair = 0 ; ePair < NewNumPairs ; ePair++ ) { for (int eInp = 0 ; eInp < InputSize ; eInp++ ) NewInput[eInp] = fBuf[ePair * PairSize + eInp]; for (int eOut = 0 ; eOut < OutputSize ; eOut++ ) NewOutput[eOut] = fBuf[ePair * PairSize + InputSize + eOut]; AddPair(NewInput, NewOutput); }

delete[] fBuf; delete[] iBuf; delete[] NewInput; delete[] NewOutput; delete File;

return true;}

#endif

ANEXO 2 – TEL A DO PROGRAMA DE TESTE