desarrollo-ia

¿Alguna vez has deseado o necesitado extraer información específica de una nota de voz del usuario? Bueno, puede haber una manera fácil.

El objetivo de este artículo es familiarizarte con el desarrollo del procesamiento de entradas de voz en Android. Por lo tanto, este artículo te permitirá comprender cómo transformar la entrada del usuario en texto y, lo más importante, cómo analizar y extraer información relevante del mismo.

Para lograr tal efecto, utilizaremos la interfaz nativa de Speech API en Android y la herramienta de terceros de PNL (procesamiento de lenguaje natural) llamada StanfordNLP.

Antes de pasar a más detalles, establezcamos un par de requisitos razonables.

Tabla de contenidos

Prerrequisitos:

  • Comprensión básica de cómo funciona PLN (procesador de lenguaje natural).
  • Experiencia básica con el desarrollo de Android y Kotlin.

Muy bien, ahora vamos a ello

Hay varias formas de procesar texto en aplicaciones cliente como la que crearemos ahora, pero hay un par de preguntas importantes sobre las que quizás ya te estés preguntando.

¿Dónde tendrá lugar el procesamiento?

Los procesadores de PNL generalmente requieren una alta potencia de procesamiento y se consideraron inadecuados para funcionar en teléfonos móviles. Si bien esto sigue siendo cierto, los teléfonos móviles están registrando mejores y mejores capacidades de procesamiento y obtienen cada vez más memoria: RAM. Quiero decir, diablos, ¡el Samsung S20 Ultra presenta 12GB de RAM, que es más de lo que la mayoría de los portátiles promedio tienen hoy en día!

Hoy intentaremos superar los límites y permitir que todo el proceso de PLN participe del lado del cliente, en los dispositivos Android en nuestro caso. Ten en cuenta que la recomendación oficial sigue siendo descargar todo este trabajo de procesamiento en un servidor en lugar de alojarlo en el propio dispositivo.

¿Qué herramienta debo usar?

Si bien hay muchas posibilidades, nos centraremos en las más accesibles, que son API’s como OpenNLP o StanfordNLP. Estas API básicamente reciben un texto y envían información sobre ese texto: oraciones, palabras, partes del discurso, lemas, etc.

Apache OpenNLP representa una opción viable, pero requiere mucha potencia de procesamiento y tiene un tiempo de inicialización bastante lento en Android, de 20 a 40 segundos en algunos de mis dispositivos. Ahora, esto no es algo relevante porque, como se explicó anteriormente, en la mayoría de los casos, ¡este tipo de procesamiento no debe hacerse en el cliente!

Afortunadamente, estamos de enhorabuena con la herramienta más fácil y rápida (adivinó): StanfordNLP, ya que se basa en el principio de una pipeline y tiene tiempos de inicialización realmente bajos (de 1s en dispositivos buenos a 4–5s en dispositivos más lentos) y tiempos de ejecución decentes (de 3 a 4 ms a unos pocos cientos según las tareas). Básicamente, le dices a la pipeline qué operaciones ejecutar en el texto, ¡y eso es todo!

Muy bien, suficiente con el jibber jabber. ¡Establezcamos una meta!

¿Por qué no creamos una aplicación de Android fácil que tome entradas de voz, las transforme en texto e identifique sus sustantivos?

¡Muéstrame el código!

Ya casi llegamos, espera. Ahora que sabemos lo que queremos hacer, primero separemos las tareas que tenemos que resolver:

  • Agregar dependencias.
  • Convierta la entrada de voz del usuario en texto.
  • Procesar el texto e identificar sustantivos.

¡Listo, vamos a ello!

Dependencias

Mientras que para la API de voz no debemos hacer nada, para StanfordNLP necesitamos agregar las siguientes líneas en el build.gradle archivo de la aplicación:

dependencies {        ...              implementation group: 'edu.stanford.nlp', name: 'stanford-corenlp', version: '3.5.0'      implementation group: 'edu.stanford.nlp', name: 'stanford-corenlp', version: '3.5.0', classifier: 'models'  }

Convierte la entrada de voz del usuario en texto

Como se mencionó anteriormente, utilizaremos la API de voz para obtener la entrada del usuario al registrar a SpeechRecognizer. También estableceremos un RecognitionListener el reconocedor para establecer algunas acciones una vez que tengamos algunos resultados:

    private var speechRecognizer: SpeechRecognizer? = null        ...        private fun listenToSpeechInput() {          speechRecognizer = SpeechRecognizer.createSpeechRecognizer(context)          speechRecognizer?.setRecognitionListener(object: RecognitionListener {              ...              override fun onResults(p0: Bundle?) {                  speechRecognizer?.stopListening()                  requestExtractQuery(p0, true)              }          })      }

No te preocupes por la requestExtractQuery() llamada por ahora, nos pondremos en contacto pronto.

Ahora, para tener resultados, necesitamos la entrada del usuario. Por lo tanto, cada vez que el usuario solicite un sonido de entrada, lanzaremos en la intención ACTION_RECOGNIZE_SPEECH y también activaremos la speechRecognizer de los resultados llamando a startListening en la intención que deseamos:

    private fun promptSpeechInput() {          val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)          intent.putExtra(              RecognizerIntent.EXTRA_LANGUAGE_MODEL,              RecognizerIntent.LANGUAGE_MODEL_FREE_FORM          )          intent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, activity?.application?.packageName?: "")          speechRecognizer?.startListening(intent)      }

¡Perfecto! ¡Ahora estamos recibiendo la entrada de voz del usuario y la estamos convirtiendo en texto! Podemos acceder al texto resultante descomprimiendo el conjunto de datos recibido y obteniendo el resultado más seguro de la matriz de resultados de texto:

 private fun requestExtractQuery(bundle: Bundle?) {          val data = bundle?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)          data?.let { results ->              val resultedText =  results[0] // First we set the text result to a TextView              txtSpeechInput.text = resultedText                                   // Secondly, we send a processing command downstream to the viewModel              // In return, we get a colorized text result with the noun highlighted and we set it again              val result: Spannable = viewModel.requestQuery(resultedText)              txtSpeechInput.text = result          }      }

Posteriormente, lo enviamos directamente para su procesamiento mediante una llamada requestQuery(resultedText) que permitirá que el texto resultante se envíe al cliente NLP.

Procesar el texto e identificar sustantivos

Antes de pasar al código responsable del procesamiento real, tenemos que inicializar el StanfordNLP client. Una buena decisión de UX sería hacer esta inicialización, que requiere mucho trabajo como cargar el modelo de idioma, en una pantalla de inicio.

    private val scope = CoroutineScope(Dispatchers.IO)        override fun onCreate(savedInstanceState: Bundle?) {          ...          scope.launch { viewModel.requestInitializeNLP() }      }

Como puedes ver, la vista delega esta inicialización en la viewModel que la delegará más abajo en la NLPClient instancia de singleton. Es importante tener en cuenta que utilizamos un coroutine scope de IO porque la inicialización del cliente es una operación pesada. Veamos cómo se puede inicializar un cliente StanfordNLP simplemente pasando una serie de anotaciones:

val props = Properties()  props.setProperty("annotators", "tokenize, ssplit, pos, lemma")  instance = NLPClient(StanfordCoreNLP(props))

¿Cómo sabemos qué anotadores pasar? Bueno, consultamos la documentación. Sabemos que necesitamos tokenizar nuestro texto para obtener oraciones individuales y, lo que es más importante, palabras individuales. Para lograr eso usamos ambos tokenize y ssplit. También sabemos que necesitamos obtener la parte del discurso de cada token, por lo que usamos el pos anotador. ¿Y por qué no hacer un esfuerzo adicional y extraer la raíz de la raíz con el simple propósito de obtener la forma singular de cualquier sustantivo plural mediante el uso lemma? Pasamos estas propiedades al constructor y terminamos aquí, ¡tenemos al cliente listo para trabajar!

Ahora volvamos al procesamiento de texto, que es básicamente lo que todos hemos estado esperando: tenemos el texto, pero necesitamos extraerle los sustantivos:

    private val pipeline: StanfordCoreNLP          ...      fun extractRootNoun(text: CharSequence): String? {          val input = text.toString()          val document = Annotation(input)          pipeline.annotate(document)                    // Get split sentences          val sentences: List<CoreMap> = document.get(CoreAnnotations.SentencesAnnotation::class.java)          // Get the tokens for the sentences          sentences.flatMap { sentence ->              sentence.get(CoreAnnotations.TokensAnnotation::class.java)          }.forEach { token ->              // Get the text of each token              val word = token.get(CoreAnnotations.TextAnnotation::class.java)              // Get the corresponding POS tag of the token              val pos = token.get(CoreAnnotations.PartOfSpeechAnnotation::class.java)                //  Check if the POS tag is matched to a noun              val partOfSpeechTag = pos.toString()              if(partOfSpeechTag == "NN" || partOfSpeechTag == "NNS") {                    // We found it, but we want the root form of the token which is the lemma                      val lemmatizedValue = token.lemma().toString()                      return lemmatizedValue              }          }          return null      }

Inicialmente, lanzamos el pipeline archivo con un documento del texto que necesitamos procesar. Desde este punto, aplicamos secuencialmente los anotadores conocidos al resultado de la pipeline. Primero obtenemos el sentences y luego iteramos a través tokens de la oración; por simplicidad, asumiremos que solo tenemos una oración y un sustantivo contenido.

Luego, iteramos sobre la tokens oración y extraemos su valor, que es el mismo word. Muy bien, tenemos la palabra, así que seguimos aplicando anotaciones, esta vez extrayendo la parte del discurso y verificando si lo identificado pos para un sustantivo. Una vez hecho esto, simplemente tenemos que hacer una cosa más, extraer el sustantivo raíz llamando al método token.lemma .

Vamos a ver cómo funciona:

¡Ya hemos terminado! Ha sido fácil, ¿verdad?

Si tienes prisa y deseas omitir esta parte, ves directamente al final de este artículo para encontrar un repositorio público con todo el código presentado anteriormente. ¡Gracias por tu tiempo!

Bien. Bueno… no tanto. Convertimos la entrada de sonido del usuario en texto, la procesamos y extraemos el primer sustantivo que pudimos encontrar. Pero, ¿cómo funciona verdaderamente la máquina?

Esta es una pregunta más complicada a la que incluso tengo problemas para responder, ya que la transformación de voz y el dominio de PLN son áreas muy amplias y complejas. En realidad, ambas todavía están en investigación activa y si te consideras un apasionado, puedo darte algunos puntos de partida con respecto a este artículo:

  • La interfaz de reconocimiento de voz (o transformación de voz a interfaz de texto) que se utiliza en esta muestra con el nombre de Speech API es un sistema muy complejo que cuenta con modelos entrenados debajo. Probablemente lo sabías, pero ¿sabes cómo funciona un sistema básico de transformación de voz? Se basa en un sistema probabilístico que se basa en un modelo oculto de Markov que tiene en cuenta las características fonéticas y acústicas, así como la información estadística de colocación de palabras dada por los modelos de idiomas previamente entrenados. Si deseas comprender las matemáticas básicas detrás de este proceso, te recomiendo que consultes el Procesamiento de audio y el Reconocimiento de voz: Conceptos, técnicas y resúmenes de investigación en el capítulo 2.3 Sistema de reconocimiento automático de voz.
  • StanfordNLP es un sistema realmente complejo y tiene un montón de características desde la perspectiva del procesamiento. Puede realizar tareas aún más avanzadas, como el uso de modelos entrenados en NER (reconocimiento de entidad con nombre) para extraer palabras relacionadas con personas, lugares, clima, fechas, etc. Una de las tareas más complejas utilizadas en esta muestra es extraer la parte del discurso de las palabras. Stanford NLP hace esto mediante el uso de un MEMM (Modelo de Markov de entropía máxima) en un proceso probabilístico que te permite tener información estadística sobre diferentes características al asignar partes del discurso a las palabras. Puedes obtener más información sobre las matemáticas y la mecánica detrás de esto al consultar Procesamiento del habla y el lenguaje Una introducción al procesamiento del lenguaje natural, la lingüística computacional y el reconocimiento del habla en el capítulo 5 Etiquetado de parte del habla.

Basta de hablar, ¿dónde está el código?

No te preocupes, no te decepcionaré solo con fragmentos. A continuación puedes encontrar un repositorio con esta muestra exacta y todo el código anterior.

catalinghita8/voice-text-nlp-stanford

The repository showcases an android application that transforms a voice command to text and identifies correspondent…

github.com

Sin embargo, ten en cuenta que el repositorio anterior también contiene un sistema de registro junto con la lógica presentada anteriormente. Por lo tanto, para un primer lector, te aconsejaría que ignores todo lo relacionado con el sistema de registro, ya que representa una herramienta de interfaz de usuario para las personas que realmente ejecutan la aplicación para comprender mejor lo que sucede detrás del escenario.

¡Gracias por su tiempo y espero que haya ayudado!

Nota final:

No recomendaría usar dicha implementación en producción ya que una parte de los dispositivos podría tener problemas para proporcionar dicha potencia informática. Además, debido a que los modelos se cargan localmente, el tamaño de la aplicación crece con el tiempo y ya no es adecuado para la producción. Dicho esto, ¡usa el contenido que te he compartido para experimentar, aprender y divertirte!

Por Catalin Ghita

Desarrollador de dispositivos móviles apasionado y con visión de futuro, centrado principalmente en Android y con más de 4 años de experiencia en la arquitectura y el desarrollo de aplicaciones. Dedicado a implementar y adoptar continuamente nuevas tecnologías para maximizar la velocidad y la eficiencia del desarrollo.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *