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