https://d226lax1qjow5r.cloudfront.net/blog/blogposts/building-a-machine-learning-model-for-answering-machine-detection-dr/Building-a-Machine-Learning-Model-for-Answering-Machine-Detection.png

Creación de un modelo de aprendizaje automático para responder a la detección automática

Publicado el May 12, 2021

Tiempo de lectura: 6 minutos

¿Alguna vez has necesitado un modo de detectar cuándo un contestador automático estaba en una llamada de voz? ¿No? No pasa nada. A mí sí.

Requisitos previos

Este post asume que usted tiene experiencia básica en Python, así como tener una comprensión muy básica de aprendizaje automático. Vamos a repasar algunos conceptos básicos sobre el aprendizaje automático, y hemos vinculado a más recursos a lo largo de este post.


Hace unas semanas, recibí una solicitud de uno de nuestros ingenieros de ventas sobre un servicio de detección de contestadores automáticos para un cliente. Querían una forma de enviar un mensaje a un contestador automático cuando la llamada saltaba al buzón de voz.

He hecho algunas investigaciones sobre esto, y parece posible, pero no pude encontrar nada sobre CÓMO se hizo esto. Así que decidí averiguarlo...

La primera idea fue construir un modelo de aprendizaje automático que detectara cuándo se oye el beep sonido de un contestador automático. En este post, repasaremos cómo se entrenó el modelo y se desplegó en una aplicación.

Datos de formación

Antes de empezar a construir un modelo de aprendizaje automático, necesitamos tener algunos datos. Para este problema, tenemos que tener un montón de archivos de audio con el contestador automático beep sonidos, como este:

https://soundcloud.com/user-872225766-984610678/7eaeb600-0202-11e9-bb68-51880c8718e4 o esto:

https://soundcloud.com/user-872225766-984610678/7eaeb600-0202-11e9-bb68-51880c8718e4

También necesitamos incluir muestras que no incluyan el sonido bip:

https://soundcloud.com/user-872225766-984610678/7eaeb600-0202-11e9-bb68-51880c8718e4

Como este tipo de datos no parece existir en Internet, necesitábamos reunir el mayor número posible de muestras de pitidos y otros sonidos de llamadas para entrenar nuestro modelo. Para ello, he creado una página web que permite a cualquiera grabar su mensaje de bienvenida al buzón de voz.

Cuando llames al número de Vonage, la aplicación creará una llamada saliente al mismo número. Cuando se recibe la llamada, sólo tienes que enviarla directamente al correo de voz. A partir de ahí, grabamos la llamada utilizando la record acción y guardamos el archivo en un bucket de Google Cloud Storage. Después de reunir un montón de ejemplos, podemos empezar a ver los datos.


En cualquier proyecto de aprendizaje automático, una de las primeras cosas que hay que hacer es examinar los datos y asegurarnos de que podemos trabajar con ellos.

Como es audio, no podemos mirar directamente, pero podemos visualizar los archivos de audio mediante un mel-espectrograma, que tiene este aspecto:

Mel Spectogram

Un espectrograma mel muestra una gama de frecuencias (la más baja en la parte inferior de la pantalla y la más alta en la parte superior) y muestra el volumen de los eventos a diferentes frecuencias. En general, los eventos ruidosos aparecerán brillantes y los silenciosos, oscuros.

Tendremos que cargar unos cuantos archivos de ambos tipos de sonidos, trazarlos y ver cómo quedan. Para mostrar el mel-espectrograma, utilizaremos un paquete de Python llamado Librosa para cargar la grabación de audio y, a continuación, trazaremos el mel-espectrograma utilizando matplotlibotro paquete de Python para trazar diagramas y gráficos.

import glob
import librosa
import matplotlib.pyplot as plt
%matplotlib inline

def plot_specgram(file_path):
  y, sr = librosa.load(file_path)
  S = librosa.feature.melspectrogram(y=y, sr=sr, n_mels=128,fmax=8000)
  plt.figure(figsize=(10, 4))
  librosa.display.specshow(librosa.power_to_db(S,ref=np.max),y_axis='mel', fmax=8000,x_axis='time')
  plt.colorbar(format='%+2.0f dB')
  plt.title(file_path.split("/")[-2])
  plt.tight_layout()

sound_file_paths = [
                    "answering-machine/07a3d677-0fdd-4155-a804-37679c039a8e.wav",
                    "answering-machine/26b25bb7-6825-43e7-b8bd-03a3884ed694.wav",
                    "answering-machine/2a685eda-8dd9-4a4d-b00e-4f43715f81a4.wav",
                    "answering-machine/55b654e5-7d9f-4132-bc98-93e576b2d665.wav",
                    "speech-recordings/110ac98e-34fa-42e7-bbc5-450c72851db5.wav",
                    "speech-recordings/3840b850-02e6-11e9-aa3d-ad1a095d8d72.wav",
                    "speech-recordings/55b654e5-7d9f-4132-bc98-93e576b2d665.wav",
                    "speech-recordings/81270a2a-088b-4e3c-9f47-fd927a90b0ab.wav"
                    ]

for file in sound_file_paths:
  plot_specgram(file)

Veamos qué aspecto tiene cada archivo de audio.

Audio files

Se puede distinguir claramente qué archivo de audio es un beep y cuál es simplemente speech.


Antes de entrenar nuestro modelo, tomaremos todas las grabaciones que tengamos tanto de beeps y de los no pitidos, que se etiquetan como speechy convertiremos cada grabación en un vector de Numbers, ya que nuestro modelo sólo aceptará Numbers, no imágenes.

Para calcular los datos, utilizaremos los coeficientes cepstrales de frecuencias mel (MFCC) de cada muestra. Después, guardaremos este valor en un csv para no tener que volver a calcular los MFCC de nuevo.

Para cada muestra de audio, el csv contendrá la ruta a la muestra de audio, la etiqueta de la muestra de audio(beepo speech), el MFCC y la duración de la muestra de audio (utilizando la función get_duration en librosa). También probamos algunas otras características de audio, incluido el croma, contraste y tonnetz). Sin embargo, estas características no se utilizaron en la última versión del modelo.

Echemos ahora un vistazo a las 5 primeras filas del csv, para ver qué aspecto tienen los datos.

CSV data

Cada fila contiene un vector de 1 dimensión de cada una de las características de audio. Esto es lo que utilizaremos para entrenar nuestro modelo.

Formación

Ahora tomaremos estos datos y entrenaremos un modelo con ellos. Utilizaremos el paquete Scikit-learn para realizar nuestro entrenamiento. Scikit-learn es un gran paquete que le permite construir modelos simples de aprendizaje automático sin tener que ser un experto en aprendizaje automático.

Para cada modelo, tomamos nuestro marco de datos, que contenía la etiqueta de cada archivo de audio, (beep, speech), con el MFCC de cada muestra, lo dividimos en un conjunto de datos de entrenamiento y otro de prueba, y ejecutamos cada modelo con los datos.

def train(features, model):
  X, y = generateFeaturesLabels(features)
  X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=42)

  model.fit(X_train, y_train)
  print("Score:",model.score(X_test, y_test))

  cross_val_scores = cross_val_score(model, X, y, cv=5, scoring='f1_macro')
  print("cross_val_scores:", cross_val_scores)
  print("Accuracy: %0.2f (+/- %0.2f)" % (cross_val_scores.mean(), cross_val_scores.std() * 2))

  predictions = model.predict(X_test)

  cm = metrics.confusion_matrix(y_test, predictions)
  plot_confusion_matrix(cm, class_names)

  return model

La función train toma una lista de características que queremos utilizar, que es sólo MFCC de la muestra de audio, así como el modelo que queremos entrenar. A continuación, imprimimos nuestra puntuación, que es lo bien que funcionó el modelo. También imprimimos la puntuación de validación cruzada. Así nos aseguramos de que nuestro modelo se ha entrenado correctamente. La función plot_confusion_matrix traza una matriz de confusión que muestra exactamente qué acertó y qué no acertó el modelo.

Confusion Matrix

A continuación probamos los siguientes modelos e incluimos su precisión (puntuación de 0-100% sobre lo bien que lo hizo el modelo).

Todos estos modelos funcionaron muy bien, excepto Support Vector Machines. El mejor fue Gaussian Naive Bayes, así que utilizaremos ese modelo. En nuestra matriz de confusión anterior, de los 67 ejemplos, 40 muestras que se predijeron como a beep eran en realidad beepsy 22 muestras que se predijeron como speech eran, de hecho speech ejemplos. Sin embargo, 1 ejemplo que se predijo que era a beep era en realidad speech.

Después de tener nuestro modelo, tenemos que guardarlo en un archivo, y luego importar este modelo en nuestra aplicación VAPI.

import pickle
filename = "model.pkl"
pickle.dump(model, open(filename, 'wb'))

Creación de la aplicación

La última parte consiste ahora en integrar nuestro modelo en una aplicación VAPI.

Vonage API Account

To complete this tutorial, you will need a Vonage API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the Vonage API Dashboard.

This tutorial also uses a virtual phone number. To purchase one, go to Numbers > Buy Numbers and search for one that meets your needs.

Crearemos una aplicación que permita al usuario marcar un número de Vonage. Luego le pediremos al usuario que ingrese un número de teléfono para llamar. Una vez ingresado el número, conectaremos esa llamada a la conversación actual y nos conectaremos a nuestro websocket. Uso de websockets de Vonagepodemos transmitir la llamada de audio a nuestra aplicación.

En primer lugar, tenemos que cargar nuestro modelo en nuestra aplicación.

loaded_model = pickle.load(open("models/model.pkl", "rb"))

Cuando el usuario marca por primera vez el número de Vonage, devolvemos un NCCO con lo siguiente:

class EnterPhoneNumberHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    def get(self):
        ncco = [
              {
                "action": "talk",
                "text": "Please enter a phone number to dial"
              },
              {
                "action": "input",
                "eventUrl": ["https://3c66cdfa.ngrok.io/ivr"],
                "timeOut":10,
                "maxDigits":12,
                "submitOnHash":True
              }

            ]
        self.write(json.dumps(ncco))
        self.set_header("Content-Type", 'application/json; charset="utf-8"')
        self.finish()

Primero enviamos una acción de texto a voz que pide al usuario que introduzca un número de teléfono. Cuando se introduce el número de teléfono, obtenemos los dígitos de la url https://3c66cdfa.ngrok.io/ivr url.

class AcceptNumberHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    def post(self):
        data = json.loads(self.request.body)
        ncco = [
             {
             "action": "connect",
              "eventUrl": ["https://3c66cdfa.ngrok.io"/event"],
               "from": NEXMO_NUMBER,
               "endpoint": [
                 {
                   "type": "phone",
                   "number": data["dtmf"]
                 }
               ]
             },
              {
                 "action": "connect",
                 "eventUrl": ["https://3c66cdfa.ngrok.io/event"],
                 "from": NEXMO_NUMBER,
                 "endpoint": [
                     {
                        "type": "websocket",
                        "uri" : "ws://3c66cdfa.ngrok.io/socket",
                        "content-type": "audio/l16;rate=16000"

                     }
                 ]
               }
            ]
        self.write(json.dumps(ncco))
        self.set_header("Content-Type", 'application/json; charset="utf-8"')
        self.finish()

Una vez introducido el número de teléfono, recibiremos una llamada desde la https://3c66cdfa.ngrok.io/ivr url. Aquí tomamos el número de teléfono introducido por el usuario desde data["dtmf"] y realizamos una acción acción de conexión a ese número de teléfono, luego realizamos otra acción de conexión a nuestro websocket. Ahora nuestro websocket es capaz de escuchar la llamada.

A medida que la llamada se transmite al websocket, tenemos que capturar fragmentos de voz mediante la Detección de Actividad de Voz, guardarlos en un archivo wave y realizar nuestras predicciones en ese archivo wav utilizando nuestro modelo entrenado.

class AudioProcessor(object):
    def __init__(self, path, rate, clip_min, uuid):
        self.rate = rate
        self.bytes_per_frame = rate/25
        self._path = path
        self.clip_min_frames = clip_min // MS_PER_FRAME
        self.uuid = uuid
    def process(self, count, payload, id):
        if count > self.clip_min_frames:  # If the buffer is less than CLIP_MIN_MS, ignore it
            fn = "{}rec-{}-{}.wav".format('', id, datetime.datetime.now().strftime("%Y%m%dT%H%M%S"))
            output = wave.open(fn, 'wb')
            output.setparams((1, 2, self.rate, 0, 'NONE', 'not compressed'))
            output.writeframes(payload)
            output.close()
            self.process_file(fn)
            self.removeFile(fn)
        else:
            info('Discarding {} frames'.format(str(count)))
    def process_file(self, wav_file):
        if loaded_model != None:
            X, sample_rate = librosa.load(wav_file, res_type='kaiser_fast')
            mfccs = np.mean(librosa.feature.mfcc(y=X, sr=sample_rate, n_mfcc=40).T,axis=0)
            X = [mfccs]
            prediction = loaded_model.predict(X)
            if prediction[0] == 0:
                beep_captured = True
                print("beep detected")
            else:
                beep_captured = False

            for client in clients:
                client.write_message({"uuids":uuids, "beep_detected":beep_captured})

        else:
            print("model not loaded")
    def removeFile(self, wav_file):
         os.remove(wav_file)

Una vez que tenemos un archivo wav, usamos librosa.load para cargar el archivo y, a continuación, utilizamos la función librosa.feature.mfcc para generar el MFCC de la muestra. A continuación, llamamos a loaded_model.predict([mfccs]) para hacer nuestra predicción. Si la salida de esta función es 0, a beep ha sido detectado. Si la salida es 1entonces es speech. A continuación, generamos una carga JSON que indica si se ha detectado a beep y los uuids de la conversación. De este modo, nuestra aplicación cliente puede enviar un TTS a la llamada, utilizando los uuids.

Cliente Websocket

El paso final es construir un cliente que se conecte al websocket, observe cuando se detecta un pitido, y envíe un TTS a la llamada, cuando se detecta el buzón de voz.

Fuente

En primer lugar, tenemos que conectarnos al websocket.

ws = websocket.WebSocketApp("ws://3c66cdfa.ngrok.io/socket",
on_message = on_message,
on_error = on_error,
on_close = on_close)
ws.on_open = on_open

ws.run_forever()

A continuación, nos limitamos a escuchar cualquier mensaje entrante de nuestro websocket.

def on_message(ws, message):
    data = json.loads(message)
    if data["beep_detected"] == True:
        for id in data["uuids"]:
            response = client.send_speech(id, text='Answering Machine Detected')

        time.sleep(4)
        for id in data["uuids"]:
            try:
                client.update_call(id, action='hangup')
            except:
                pass<a href="https://www.nexmo.com/wp-content/uploads/2019/02/amd-confusion-matrix.png"><img src="https://www.nexmo.com/wp-content/uploads/2019/02/amd-confusion-matrix-600x300.png" alt="" width="300" height="150" class="alignnone size-medium wp-image-28012" /></a>

<a href="https://www.nexmo.com/wp-content/uploads/2019/02/amd-df.png"><img src="https://www.nexmo.com/wp-content/uploads/2019/02/amd-df-600x300.png" alt="" width="300" height="150" class="alignnone size-medium wp-image-28015" /></a>

<a href="https://www.nexmo.com/wp-content/uploads/2019/02/amd-eda.jpg"><img src="https://www.nexmo.com/wp-content/uploads/2019/02/amd-eda-600x300.jpg" alt="" width="300" height="150" class="alignnone size-medium wp-image-28018" /></a>

Analizaremos el mensaje entrante como JSON y comprobaremos si la propiedad beep_detected es True. Si lo es, entonces se ha detectado un beep fue detectado. Entonces enviaremos un TTS a la llamada diciendo 'Answering Machine Detected', luego realizaremos una hangup acción en la llamada.


Conclusión

Hemos mostrado cómo construimos un modelo de detección de contestadores automáticos con un 96% de precisión, utilizando unas pocas muestras de audio de beeps y speech para entrenar nuestro modelo. Esperamos haber mostrado cómo puedes utilizar el aprendizaje automático en tus proyectos. ¡Que lo disfrutes!

Compartir:

https://a.storyblok.com/f/270183/150x150/a3d03a85fd/placeholder.svg
Tony HungAntiguos alumnos de Vonage

Desarrollador IOS convertido en entusiasta de la Ciencia de Datos / Aprendizaje Automático. Quiero que la gente entienda qué es el aprendizaje automático y cómo podemos utilizarlo en nuestras aplicaciones.