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