fundação escola técnica liberato salzano vieira

Transcrição

fundação escola técnica liberato salzano vieira
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.
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 incorporadas ao cotidiano. Muitas pessoas, porém, não conseguem acompanhar este desenvolvimento 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 possibilidade de comandar 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. Porém, este é um assunto complexo, com muitas variáveis a serem analisadas e otimizadas. Para facilitar o desenvolvimento do método ele foi dividido em duas etapas. A primeira 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 reconhecimento 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 representa nossas expectativas iniciais, antes do início da pesquisa propriamente dita. O segundo e o terceiro capítulos apresentam uma abordagem teórica das duas etapas do trabalho. 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 microfone conectado à placa de som. Mais precisamente, desejamos fazer com que determinadas 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 equipamentos 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 utilizadas, 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 possibilidade 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 possibilidades 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 computador. 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 denominaremos 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 armazenados e possibilitando 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 fazer o sistema funcionar. Faremos então os ajustes e calibrações necessários, e acreditamos 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 pesquisa, em função de alguma suposição feita inicialmente que não corresponda corretamente à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 professora 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 basicamente de computadores para o desenvolvimento dos diversos programas. Como precisamos ser capazes de gravar sons, estes computadores deverão possuir recursos de
multimídia.
1.5.
Cronograma
Outubro
Setembro
Agosto
Julho
Junho
Maio
Pesquisa inicial; divisão do trabalho em partes
Pesquisa aprofundada; início do desenvolvimento de cada parte
Conclusão de cada parte e sua união
Ensaios e ajustes finais
Confecçã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, contendo 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 vibrem, 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 caracterizados 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, apresentando características próprias bem definidas, diferenciando-o dos demais.
Fonemas vozeados apresentam uma estrutura harmônica, onde distingue-se claramente 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 freqüencial do fonema, uma vez que a informação desejada está nela contida. Contudo, a
grande variabilidade 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: eliminar 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 componentes freqüenciais de baixa amplitude distribuídas quase que aleatoriamente ao longo 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 é percebido 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 variabilidade, juntamente com o grande volume de dados, inviabiliza a utilizaçã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 caracterizem 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 compõem o sinal. A base matemática para esta análise á transformada de Fourier. Para sinais
discretos, representados através de um vetor, utiliza-se a DFT (Discrete Fourier Transform, Transformada Discreta de Fourier), dada por:
N −1
H [k ] = ∑ h[n]e − j 2πkn / N
n=0
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, variando suas características freqüenciais ao longo do tempo. Porém em um intervalo suficientemente curto pode ser considerada como tal. Desta maneira, aplica-se a FFT sobre
as janelas temporais (que podem variar de 10 a 50 milissegundos) 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á exemplificado 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:
E=
1
N
N −1
∑h
2
[ n]
n =0
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 extraí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 banco de dados. Problemas como este exigem uma solução mais versátil, capaz de adaptarse a todas as variações possíveis na pronúncia. Algoritmos voltados à inteligência artificial 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 forma mais semelhante ao cérebro humano.
Neste sentido, duas técnicas ganharam grande destaque nas duas últimas décadas: 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 trabalhando 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érebro. A unidade básica de nosso cérebro, o neurônio, apresenta uma região onde informaçõ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 resultado 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, utilizada para obter a saída do neurônio, é chamada de função de ativação. As funções de ativação mais utilizadas são funções do tipo sigmoidal (com forma de S). A mais utilizada de todas é a função logística: f ( x) =
1
.
1 + e−x
Figura 3.2 – A função logística
A maior vantagem desta função é sua derivada, facilmente encontrada:
f ' ( x ) = f ( x ).(1 − f ( x))
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ãodeterminí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 utilizada atualmente em problemas práticos é a feedforward, que pode ser implementada em processadores comuns e, comparando-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. É importante 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 previsã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 treinamento é 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 treinamento mais utilizada para o treinamento de redes neurais feedforward é a Error
Backpropagation (retropropagação de erros). A idéia deste algoritmo é atualizar os pesos 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:
δ j = f ' ( sp j ).(d j − o j )
∂E
= −oi .δ j
∂Pji
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 complexo, 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:
δ j = f ' ( sp j ).∑ (δ k .Pkj )
k
∂E
= −oi .δ j
∂Pji
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 modificados 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 é utilizar 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 treinamento é 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 Windows. Para o desenvolvimento do software, foram utilizados 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 Programming 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
facilidade 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 extremamente 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, deixando-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: waveInOpen, waveInClose, waveInStart, waveInStop e waveInReset responsáveis pelo controle do dispositivo, e as funções waveInPrepareBuffer, waveInUnprepareBuffer e waveInAddBuffer, 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 valores adquiridos, fazendo com que o programa tenha um controle dinâmico sobre a memó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: WaveIn e Recorder. 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 dispositivo de entrada.
A classe WaveIn 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 utilizada
para abrir um dispositivo de entrada de áudio, fornecendo os parâmetros quanto ao formato desejado dos dados e a janela que receberá as mensagens do dispositivo. Já a função Close deve ser utilizada para fechar o dispositivo, enquanto as funções Start e
Stop são utilizadas no controle de gravação dos dados.
21
Apesar desta funcionalidade, esta classe não deve ser utilizada diretamente, tendo seu uso voltado para uma variável membro da classe Recorder, responsável por
uma gama maior de funções.
A classe Recorder 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 Start e Stop, que controlam o andamento da gravação propriamente dita,
além da função FillVector, 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 Recorder, especificando os parâmetros desejados
para a digitalização. Para o iniciar o processo, deve acionar a função Start, especificando um handle de uma janela que recebe os dados. Esta janela recebe a mensagem
MM_WIM_DATA, indicando que um buffer está disponível, sendo que é obtido através da
função FillVector, fornecida pelo objeto Recorder.
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 realizados os cálculos iniciais envolvendo os dados necessários para a utilização do algoritmo. Além do construtor, a classe conta também com duas funções Transform sobrecarregadas: uma que aceita como parâmetro um vetor de valores do tipo complex, e
outra com valores do tipo double. 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, enquanto 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 detecte os limites das palavras fez-se necessário. O método escolhido foi baseado no cálculo da potência do sinal sendo captado. Foi definido assim um limiar de energia, a partir do qual considerou-se uma palavra sendo dita.
4.2.3. Normalização das amplitudes
O sistema apresentou melhora significativa no desempenho quando as amplitudes foram normalizadas, antes da aplicação das FFTs. Sem esta normalização, o reconhecimento 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 segundo) 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 amplitude. Para isso elevou-se os dados dos vetores espectrais à diversas potências. A potê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 feedforward 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, é utilizada. 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 utilizaçã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, correspondentes 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 camada oculta, o valor que mostrou-se mais adequado foi o de 80 neurônios.
5. RESULTADOS
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 utilizar o programa e passa a pronunciar as
palavras da maneira mais adequada.
Foi realizado um segundo teste com dados de treinamento compostos pelas palavras /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 reconhecimento para próximo de 60%, variando até 70% em função do locutor e da palavra 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 desenvolvimento de uma pesquisa nesta área exige muito trabalho. Acreditamos os resultados
obtidos foram muito bons, considerando a complexidade da proposta e o tempo dedicado 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 utilizado 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 desenvolvido. 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 possibilidade da
utilização de novas técnicas.
Redes neurais, por exemplo, têm sido pouco exploradas nesta área. Segundo alguns 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 adequada 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 adaptasse ao locutor, de modo que a eficiência do sistema fosse sendo incrementada automaticamente durante seu uso. Redes neurais com esta característica são mais complexas, mas perfeitamente possíveis.
Sabemos também que o sistema carece de recursos que permitam ignorar determinados 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. Esperamos 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 Hills,
Prentice Hall, 1989. 569 p.
3.
KOVÁCS, Zsolt. Redes Neurais Artificiais. Teoria e Aplicação. São Paulo, collegium cognitio, 1996. 139 p.
4.
LIM, Jae S. Two-Dimensional Signal and Image Processing. Engelwood Hills,
Prentice Hall, 1990. 694 p.
5.
LUFT, Joel. Reconhecimento Automático de Voz para Palavras Isoladas e Independente 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 Biomé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( WaveHeader* ) ;
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(errorText) ) ;
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 dispositivo de entrada." ;
else
errorText = _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].dwBufferLength = _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 / (_nChannels*_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 fabs( 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( _logPoints + 1 ) ;
int _2_l = 2 ;
double re, im ;
for( int l=1 ; l < _W.limit() ; l++ )
{
_W[l].resize( _Points ) ;
for( int i=0;
{
re = cos(
im = -sin(
_W[l][i] =
} ;
_2_l <<= 1 ;
} ;
i < _W[l].limit() ; i++ )
2.0 * pi * i / _2_l ) ;
2.0 * pi * i / _2_l ) ;
complex( re, im ) ;
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<_Points ; 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[ _aBitRev[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( int 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
{
sum
sum
sum
sum
}
(k--)
+=
+=
+=
+=
vet1[p]
vet1[p]
vet1[p]
vet1[p]
*
*
*
*
vet2[p++];
vet2[p++];
vet2[p++];
vet2[p++];
//
//
//
//
Fazendo desta forma, de 4 em 4, o número
de loops é reduzido. Isto deve acelerar
bastante o processo, especialmente se o
processador utilizado usar pipelining.
36
while (m--)
sum += vet1[p] * vet2[p++];
return sum;
}
#define F_TABLE_LENGTH 100
#define F_TABLE_MAX 10.0
static 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
iBuf[0] = (unsigned
iBuf[1] = (unsigned
iBuf[2] = (unsigned
short
short
short
short
int [3];
int)InputLayer->NumNrns;
int)HiddenLayer->NumNrns;
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 <
{
for (unsigned short int eInp = 0 ; eInp <
fBuf[(ePair * (InputSize + OutputSize))
for (unsigned short int eOut = 0 ; eOut <
fBuf[(ePair * (InputSize + OutputSize))
NumPairs ; ePair++ )
InputSize ; eInp++ )
+ eInp] = GetInput(ePair, eInp);
OutputSize ; eOut++ )
+ 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 – TELA DO PROGRAMA DE TESTE

Documentos relacionados