Laboratorio De Procesado De La Voz

Preview only show first 6 pages with water mark for full document please download

Transcript

Laboratorio de Procesado de la Voz Reconocimiento de palabras aisladas mediante HMM Roger Piqueras Jover Profesor: José Adrián Rodríguez D5-212 Preprocesado de las señales 1) Objetivo: Nuestro trabajo en el laboratorio se ha centrado en la parte del procesado que recibe la señal de voz para ser convertida a una secuencia discreta de símbolos que, posteriormente, serán utilizados para el entrenamiento de los modelos HMM o para el reconocimiento de palabras aisladas (las cifras del 0 al 9 en catalán) mediante estos modelos. La señal de voz es digitalizada a una frecuencia de muestreo fm=8KHz, la misma que se utiliza en telefonía. Esta señal de voz es separada en tramas de N=240 muestras (tramas de 30ms) con un solapamiento de 120 muestras (15ms). La mayor información de esta señal se encuentra en el dominio transformado, así que nos interesa obtener una representación frecuencial de cada trama. Primero calculamos su autocorrelación, y después, mediante un análisis LPC y un algoritmo recursivo obtenemos los coeficientes cepstrales (se calculan 12 coeficientes). Antes de enviar al reconocedor/entrenador estos coeficientes los enventanamos mediante una ventana coseno alzado. Con este sistema se obtienen buenos resultados, pero nosotros nos proponemos mejorarlo mediante dos procedimientos alternativos de preprocesado de la señal de voz, que serán: - MFCC (FFT + FB (filtros de Mel) + LOG + TCOS-1) - FF (FFT + FB + FF (filtrado frecuencial))1 Los dos se basan en una primera transformación frecuencial para obtener la FFT de cada trama. Posteriormente se aplica a esta FFT (en realidad al módulo de esta) un banco de filtros (filtrado de MEL) según la sensibilidad del oído humano, con lo que obtendremos un número menor de muestras (en nuestro caso 16). El sistema MFCC obtiene los coeficientes a partir de una transformada logarítmica seguida de una transformada coseno inversa, en cambio el sistema FF se basa en el filtrado frecuencial propuesto en el artículo Nadeu. Esquema del filtrado frecuencial propuesto en el artículo Nadeu 1 “Time and frequency filtering of filter-bank energies for robust HMM recognition”, CLIMENT NADEU, DUŜAN MACHO, JAVIER HERNANDO. Speech Communication 34 (2001) 93-114 Tabla extraída del artículo Picone2, donde se describen los bancos de filtros que se debe aplicar a la señal de voz previamente procesada. En ella se observa como, para alcanzar el ancho de banda deseado (ancho de banda telefónico BF=fm/2=4KHz) es necesario utilizar un número superior a 12 de coeficientes (recordar que en el sistema original trabajábamos con 12 coeficientes cepstrales). El propio autor propone trabajar con 16 filtros de la escala de Bark, concretamente con los de índices de 2 a 17. Así que trabajaremos con 16 coeficientes en los dos nuevos sistemas que nos proponemos implementar. Para los dos nuevos sistemas implementados trabajaremos con tramas de 256 muestras (calcularemos una FFT de 256 puntos), por lo que previamente rellenaremos con 0’s para pasar de 240 muestras a 256, ya que para hacer la FFT necesitamos un número de puntos que sea potencia de 2. 2 “Signal modeling techniques in speech recognition”, JOSEPH W. PICONE, Senior member, IEEE. Procedings of the IEEE, Vol. 81, nº 9. Spetember 1993. 2) Descripción del sistema inicial: El primer problema con el que nos encontramos a la hora de tratar de implementar los dos nuevos sistemas es el completo desconocimiento del programa que trabaja con el sistema inicial, por lo que antes de todo hemos procedido a analizarlo. Toda la parte de preprocesado se realiza en los programas prm y pv_prm: - prm.cpp: Este fichero trabaja con archivos de entrada de extensión *.pcm, que contienen las muestras de señal de voz muestreadas, y crea un archivo de salida con el mismo nombre y extensión donde deja las muestras procesadas. A este programa se le pueden pasar distintos parámetros, como un posible nombre (distinto al del archivo inicial) para el archivo de salida, una lista de archivos a procesar, etc. El núcleo de este ejecutable se encuentra en la función prmFile, donde se encuentra todo el procesado. Primero se abre el archivo con las muestras y el archivo donde se guardarán las muestras procesadas. Luego lee parte de la primera trama (120 muestras) y completa la trama con 120 nuevas muestras, configurándose así el enventanado. Con esta trama es con la que calcula los parámetros, que una vez calculados, guarda en el archivo que previamente había abierto. El cálculo de los parámetros lo hace la función prm_prm que se encuentra definida en el archivo pv_prm.c: /* Calcula los parámetros */ prm_prm(prmreg, x, c); En este programa sólo tenemos que cambiar los valores que definen el sistema que se va a utilizar para calcular los coeficientes. Esto es MFCC o nadeu (así hemos llamado a los nuevos sistemas), o el número de coeficientes, que ahora pasará a ser 16 (ver tabla de bandas frecuenciales de Bark, página 2). - pv_prm.c: Este es el programa que se encarga de la mayor parte del procesado de la señal muestreada de voz. Antes de nada, es importante comentar la estructura PRMREG (que se le pasa como parámetro desde prm.cpp): struct PRMREG { PRMTYPE type; float preemp; int frame_len; int param_len; int order; float xw; float *ac; float *a; float *v1; float *v2; /*Método utilizado (LPCC)*/ /*Número de muestras N=240*/ /*Número de coeficientes (12)*/ /*Señal enventanada*/ /*Correlación*/ float *wframe; float wcep; /*Cepstrum*/ }; En esta estructura es donde se encuentran los distintos vectores con los que trabajaremos (señal, señal enventanada, coeficientes lpc, etc). int prm_prm(PRMREG* prm_reg, const float *x, float *c) La función donde se realiza todo el preprocesado de señal es la función prm_prm, que devuelve los coeficientes calculados en el vector float *c. Dentro de esta función hay un case que a partir de prm_reg->type elige el sistema que se va a utilizar (inicialmente LPCC). A su vez, dentro de esta función de llama a las siguientes funciones: pre_emphasis_and_window(x, prm_reg->xw, prm_reg->frame_len, prm_reg->preemp, prm_reg->wframe); (Pre-énfasis y enventanado de la trama) ac(prm_reg->xw, prm_reg->frame_len, prm_reg->ac, prm_reg>order); (Cálculo de la autocorrelación) lpc(prm_reg->ac, prm_reg->a, prm_reg->order, prm_reg->v1, prm_reg->v2); (Cálculo de los coeficientes LPC) lpc_cepstrum(prm_reg->a, prm_reg->order, c, prm_reg>param_len); (Coeficientes cepstrales) for (i=0; iparam_len; i++) c[i] *= prm_reg->wcep[i]; (Enventanado del cepstrum) Después de calcular los coeficientes cepstrales los pasa como resultado al programa prm.cpp. Las muestras de señal a procesar (tramas de 240 muestras) se enventanan con una ventana de Hamming definida por la función hamming. Esta ventana queda también guardada en memoria dentro de la estructura PRMREG. void hamming(float *w, int length) { int i; double tmp = PI; tmp = (2*tmp)/(length-1); for (i=0; ixw256 = calloc(256,sizeof(float)); prm_reg->fft256x = calloc(256, sizeof(float)); prm_reg->fft256y = calloc(256, sizeof(float)); prm_reg->mel16 = calloc(16, sizeof(float)); prm_reg->log16 = calloc(16, sizeof(float)); prm_reg->filtros_mel = calloc(20*256, sizeof(double)); (En la función prm_alloc) free(prm_reg->xw256); free(prm_reg->fft256x); free(prm_reg->fft256y); free(prm_reg->mel16); free(prm_reg->log16); free(prm_reg->filtros_mel); Después de enventanarse la trama de 240 muestras se pasa al switch, al que se le añaden dos nuevos case: MFCC y nadeu. Dentro de la función prm_prm los dos sistemas realizan una adaptación de las muestras para pasar de 240 a 256. Consiste simplemente en llenar de ceros hasta llegar a las 256 muestras: - Case (MFCC): (FFT + FB + LOG + TCOS-1) Lo primero es pasar a tener una trama de señal enventanada con 256 muestras. for(j=0;jframe_len;j++){ prm_reg->xw256[j]=prm_reg->xw[j]; } for(j=0;j<16;j++){ prm_reg->xw256[prm_reg->frame_len+j]=0; } Calculamos la FFT de 256 puntos. Para ello programamos la función fft a partir de una propuesta de implementación sacada del libro de SiSII3, ampliándola para que pueda trabajar con el vector de partes reales y el de imaginarias por separado. Esta función recibe como parámetros las partes real e imaginaria de una señal y calcula su FFT. Al ser la señal de voz real le pasamos como parte imaginaria un vector de ceros. for(k=0;k<256;k++) { prm_reg->fft256x[k] = prm_reg->xw256[k]; prm_reg->fft256y[k] = 0; } fft(prm_reg->fft256x,prm_reg->fft256y,256,1); void fft(float *x,float *y,int m,int ind){ float Tx,Ty,Ux,Uy,Wx,Wy,tempx,tempy; int N = Nv2 Nm1 Nm2 ii,ij,k,L,Lv2,Lv2m1,N,Nv2,Nm2,Nm1,ip; 1 << m; = N >> 1; = N - 1; = N-2; /*Algoritmo bit reversed*/ ij=0; for(ii=0;ii>= 1; } ij=ij+k; } /*Cálculo de las mariposas*/ for(k=1;k<=m;k++){ L = 1 << k; Lv2 = L >> 1; Lv2m1=Lv2-1; Ux=1; Uy=1; Wx=cos(ind*PI/Lv2); Wy=-sin(ind*PI/Lv2); for (ij = 0; ij < Lv2m1; ij ++) { for (ii = ij; ii < Nm1; ii += L) { ip = ii + Lv2; mult_comp(x[ip],y[ip],Ux,Uy,&Tx,&Ty); x[ip]=x[ii]-Tx; y[ip]=y[ii]-Ty; x[ii]=x[ii]+Tx; y[ii]=y[ii]+Ty; } mult_comp(Ux,Uy,Wx,Wy,&tempx,&tempy); Ux=tempx; Uy=tempy; } mult_comp(Ux,Uy,Wx,Wy,&tempx,&tempy); Ux=tempx; Uy=tempy; } if(ind==-1){ for(ii=0;ii<=N-1;ii++){ x[ii]=x[ii]/N; y[ii]=y[ii]/N; } } } Dentro de esta función utilizamos una función sencilla que calcula el producto de dos números complejos. void mult_comp(float x,float y, float a, float b, float *outx, float *outy){ *outx=a*x-b*y; *outy=x*b+a*y; } Para poder trabajar con un solo vector de 256 muestras calculamos el módulo de la FFT y guardamos el resultado en el mismo vector donde teníamos la parte real de la FFT. modulo(prm_reg->fft256x,prm_reg->fft256y); void modulo(float *x,float *y){ int i; for(i=0;i<256;i++){ x[i]=sqrt(x[i]*x[i]+y[i]*y[i]); } } El siguiente paso es realizar el filtrado frecuencial con el banco de filtros. La tabla donde guardaremos el banco de filtros (double prm_reg->filtros_mel) la tenemos apuntada por un puntero, y hemos reservado espacio para ella en la función calloc. Realizamos el cálculo de los filtros con la función generar_filtros. Esta función la implementamos a partir de un código extraido de internet que inicializa en una matriz los 20 primeros filtros que garantizan cubrir todo el ancho de banda telefónico. En la práctica, de estos filtros solo utilizaremos los 16 que se recomiendan en el artículo Picone. generar_filtros(filtros_mel); void generar_filtros(double f_mel[20][256]){ int i, j, f1_i, f2_i, fc_i; float f2_f, f1_f, fc_f; /*Datos extraídos del artículo de Picone*/ int fc[20] = {100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1149, 1320, 1516, 1741, 2000, 2297, 2639, 3031, 3482, 4000}; int bw[20] = {100, 100, 100, 100, 100, 100, 100, 100, 100, 124, 160, 184, 211, 242, 278, 320, 367, 422, 484}; /*Generación de los filtros*/ for(i=0; i<20; i++) { fc_f=(float)fc[i]; //f:frecuencia analógica fc_i=(int)(fc_f/31.25); //k:# de muestra f2_f=(float)(fc[i]+bw[i]/2); //f=(k·8KHz)/256 --> k=f/31.25 f2_i=(int)floor(f2_f/31.25); f1_f=(float)(fc[i]-bw[i]/2); f1_i=(int)ceil(f1_f/31.25); for(j=0;jfiltros_mel,prm_reg->fft256x,prm_reg>mel16); void filtrado_mel(double *f_mel, float *s_in, float *s_out) { int i,j; for(i=0;i<16;i++) for(j=0;j<256;j++) s_out[i]+=s_in[j]*f_mel[i*256+j]; } El siguiente paso es aplicar la transformación coseno. Se debe tener en cuenta que si tenemos algún coeficiente nulo, el logaritmo nos daría infinito (-∞). Por eso, en caso de tener algún coeficiente nulo, asignamos un valor muy pequeño al logaritmo, pero nunca infinito. logaritme(prm_reg->mel16,prm_reg->log16); void logaritme(float *a,float *b){ int i; for(i=0; i<16; i++){ if(a[i]!=0) b[i]=log10(a[i]); else b[i]=-40; } } El último paso es calcular la transformada coseno inversa. Utilizamos como transformación la siguiente: coseno(prm_reg->log16,c); void coseno(float *x,float *y){ int j,k; float temp; for(j=0;j<16;j++){ temp=0; for(k=0;k<16;k++){ temp=temp+x[k]*cos((2*k+1)*j*PI/32); } y[j]=temp/2; } } Los coeficientes resultantes los colocamos en el vector c, que se nos ha pasado desde prm.cpp por referencia. - Case (nadeu) (FFT + FB + FF) Este segundo sistema es idéntico al MFCC en sus primeros pasos y también se añaden ceros para tener 256 muestras de señal enventanada. Las funciones fft, modulo y filtrado_mel son las mismas, al igual que la definición de la matriz de filtros y la función generar_filtros. El paso siguiente al filtrado con el banco de filtros es aplicar el filtrado frecuencial que se propone en el artículo Nadeu. Se trata de un filtro lineal muy sencillo, cuya transformada z es la siguiente: 16 coef. 0 1 ... 15 ... filtrado_nadeu(prm_reg->mel16,c); void filtrado_nadeu(float *mel,float *out){ int i,j; //H(z)=z-z^(-1) out[0]=mel[0]; //y[n]=x[n+1]-x[n-1] out[15]=mel[15]; //out[n]=mel[n+1]-mel[n-1]; for(i=1;i<15;i++){ out[i]=mel[i+1]-mel[i-1]; } } Adjuntamos al final el código completo de los programas prm.cpp y pv_prm.c. 4) Funcionamiento de los dos nuevos sistemas: Una vez implementados los dos sistemas pasamos a modificar el funcionamiento y llamada de los programas que intervienen en el pre-procesado de la señal para que puedan funcionar con las dos nuevas modalidades. Modificamos el programa prm.cpp de manera que al llamarlo se le pueda indicar que tipo de pre-procesado deseamos. Esto lo hacemos a través de la función getOpt. Esta función lee los distintos parámetros que se pasan al ejecutar el programa prm.exe: int getOpt(int ArgC, char *ArgV[], Directory &DirInput, Directory &DirOutput, Filename &Lista, Filename &FicPcm, Filename &FicPrm, PRMTYPE &type) { char Opcion; while ((Opcion = getopt(ArgC, ArgV, "i:o:l:c:mn")) != -1) { switch (Opcion) { case 'i': DirInput = optarg; DirInput += '/'; break; case 'o': DirOutput = optarg; DirOutput += '/'; break; case 'l': Lista = optarg; break; case 'm': //Activa el sistema MFCC type = MFCC; P = 19; break; case 'n': //Activa el sistema nadeu type = nadeu; P=19; break; case '?': return -1; } } int narg = ArgC - optind; if (Lista.empty()) { if ((narg < 1) || (narg > 2)) return -1; FicPcm = ArgV[optind++]; if (narg == 2) FicPrm = ArgV[optind++]; } else { if (narg > 0) return -1; } return 0; } Añadimos dos nuevos indicadores que se pueden pasar al ejecutar prm.exe, de manera que si añadimos –m se utilizará el sistema MFCC y si añadimos –n se utilizará el sistema nadeu. Para esto se actúa sobre el contenido de la variable type (PRMTYPE type = LPCC), que indica el tipo de sistema a utilizar y que por defecto tiene el valor de LPCC (si ejecutamos sin añadir –m o –n se utiliza por defecto el sistema LPCC), y la variable P. Esta última variable es la que nos indicará que vamos a trabajar con vectores de 16 muestras en vez de con vectores del cepstrum de 12 muestras (P tiene como valor por defecto 12). Los demás programas funcionarán igual aunque ahora trabajen con otros parámetros que no son el cepstrum, aunque es importante indicarles con que tipo de vectores van a trabajar. Por eso hay que modificar los programas que utiliza el cuantificador y indicarle que se va a trabajar con vectores de 16 muestras. Esto lo hacemos modificando también la función getOpt en los archivos pvq y pvqt de manera que, al llamar al ejecutable se le pueda indicar éste valor: int getOpt(int ArgC, char *ArgV[], Filename &Codebook, Directory &DirInput, Directory &DirOutput, Filename &Lista) { ... case 'p': P = atoi(optarg); break; //Cambio del valor de P ... } Así, cuando trabajemos con el sistema MFCC o nadeu deberemos llamar a estos programas añadiendo –p 16. Sino, el valor por defecto de P es 12. Hechos estos cambios ya se pueden utilizar los dos nuevos sistemas implementados. 5) Resultados: Después de depurar el programa varias veces y realizar varias pruebas llegamos a unos resultados no demasiado buenos. Tanto con el sistema nadeu como con el MFCC no se consigue rebajar la tasa de error de un 84%, cosa que representa un empeoramiento exagerado de las prestaciones iniciales del sistema. Hay que recordar que el sistema inicial lograba una tasa de error de sólo el 4%. Mediante el debugger comprobamos la correcta ejecución de todo el programa, y nos cercioramos de que no haya algún bucle infinito o algún for mal tratado que llene el espacio reservado en memoria. Como en principio se nos dijo que debíamos actuar sólo en los programas prm y pv_prm, por lo que no hemos analizado demasiado a fondo los programas que cuantifican los vectores. Así que podría ser que se tuviera que modificar también el código de estos programas para que trabajen con los nuevos vectores, con 16 muestras. Así, los resultados de este proyecto han sido muy poco satisfactorios, no sólo en nuestro caso sino en general. Al parecer varios de nuestros compañeros que han hecho la misma parte del proyecto han obtenido resultados igual de negativos. No hemos podido comprobar el funcionamiento del sistema completo que trabaja con la derivada y la segunda derivada puesto que el primer bloque del sistema, el nuestro, no funciona correctamente.