UD03 · Notebook 5 — Ampliación: clasificador de géneros musicales¶
Como ejemplo del uso de modelos que funcionan en audio, haremos un clasificador de géneros musicales. Para hacer esto, utilizaremos el conjunto de datos GTZAN, un conjunto de datos de 1000 muestras de audio etiquetadas con el género de la música.
Instalación de librerías¶
Para ejecutar este cuaderno, necesitaremos instalar las siguientes librerías:
%pip install "transformers<5" datasets librosa soundfile torch accelerate evaluate
import os
os.environ["WANDB_DISABLED"] = "true"
Cargamos el dataset¶
from datasets import load_dataset
gtzan = load_dataset("sanchit-gandhi/gtzan", "default")
gtzan
/usr/local/lib/python3.12/dist-packages/huggingface_hub/utils/_auth.py:94: UserWarning: The secret `HF_TOKEN` does not exist in your Colab secrets. To authenticate with the Hugging Face Hub, create a token in your settings tab (https://huggingface.co/settings/tokens), set it as secret in your Google Colab and restart your session. You will be able to reuse this secret in all of your notebooks. Please note that authentication is recommended but still optional to access public models or datasets. warnings.warn(
DatasetDict({
train: Dataset({
features: ['file', 'audio', 'genre'],
num_rows: 999
})
}) Como podemos ver, el conjunto de datos consta de 999 muestras de audio etiquetadas con el género de la música.
Los audios están en formato de 22050 Hz, y el modelo necesita 16 kHz. Las versiones recientes de datasets exigen instalar torchcodec (y un FFmpeg compatible) para decodificar audio automáticamente con la clase Audio, algo que falla con facilidad según el entorno (versión de FFmpeg, sistema operativo...). Para evitar esa fragilidad, decodificamos el audio nosotros mismos con soundfile (lee los bytes) y librosa (remuestrea a 16 kHz), y guardamos el resultado en columnas propias (array, sampling_rate) en vez de depender del tipo Audio.
import io
import soundfile as sf
import librosa
from datasets import Audio
TARGET_SR = 16000
def decodificar_audio(batch):
"""Decodifica los bytes de audio a mano (sin torchcodec) y remuestrea a 16 kHz."""
arrays, sampling_rates = [], []
for item in batch["audio"]:
data, sr = sf.read(io.BytesIO(item["bytes"]))
if data.ndim > 1:
data = data.mean(axis=1) # a mono
data = data.astype("float32")
if sr != TARGET_SR:
data = librosa.resample(data, orig_sr=sr, target_sr=TARGET_SR)
arrays.append(data)
sampling_rates.append(TARGET_SR)
return {"array": arrays, "sampling_rate": sampling_rates}
gtzan = gtzan.cast_column("audio", Audio(decode=False))
gtzan = gtzan.map(
decodificar_audio,
batched=True,
batch_size=16,
remove_columns=["audio"],
desc="Decodificando audio",
)
# (Ojo: NO usar gtzan.set_format('numpy', ...) aqui - en Colab con torchvision ya
# importado, el formateador numpy de datasets intenta importar VideoReader de
# torchvision.io y revienta si esa clase no existe en la version instalada. Basta con
# convertir a numpy con np.array(...) justo donde haga falta, mas abajo.)
Creación del conjunto de datos test¶
Para evaluar el modelo necesitaremos un conjunto de datos de prueba. Para hacer esto, dividiremos el conjunto de datos en dos partes, uno para entrenar el modelo y otro para evaluarlo.
gtzan = gtzan["train"].train_test_split(seed=42, shuffle=True, test_size=0.1)
gtzan
DatasetDict({
train: Dataset({
features: ['file', 'genre', 'array', 'sampling_rate'],
num_rows: 899
})
test: Dataset({
features: ['file', 'genre', 'array', 'sampling_rate'],
num_rows: 100
})
}) Una vez que el conjunto de datos se ha separado en dos partes, el conjunto de datos de prueba contendrá 100 muestras de audio.
A continuación, mostraremos una muestra del conjunto de datos de prueba.
muestra = gtzan['train'][0]
{**muestra, 'array': muestra['array'][:5]} # solo un fragmento, para no imprimir 480 000 numeros
{'file': '/home/sanchit/.cache/datasets/downloads/extracted/f729783d70a4541cc4c9d5649655490a9c660280bdbecddfe38a8a806c73f60e/genres/pop/pop.00098.wav',
'genre': 7,
'array': [0.08735090494155884,
0.2018338441848755,
0.479086697101593,
0.35623201727867126,
0.21140910685062408],
'sampling_rate': 16000} De cada muestra del conjunto de datos podemos ver los siguientes datos:
array: audio en forma de lista de números. El valor de cada elemento representa la amplitud de la onda en un instante de tiempo. Como elsampling_ratees de 16000 Hz, esta lista tendrá 16 000 elementos por segundo. Se convierte a numpy connp.array(...)justo donde haga falta (al pipeline de clasificación, por ejemplo).sampling_rate: la frecuencia de muestreo del audio, ya remuestreado a 16 kHz.genre: el género de la música como entero. Podemos usar el métodoint2str()delfeaturegenre()para obtener el género en formato texto.
int2str = gtzan["train"].features["genre"].int2str
int2str(gtzan['train'][0]['genre'])
'pop'
Prueba del modelo sin entrenamiento¶
Antes de comenzar a entrenar el modelo, probaremos el modelo sin entrenamiento para ver cómo se comporta. Usaremos el modelo distilhubert, un modelo previamente entrenado para clasificar el audio y fácil de refinar. Para usar el modelo usaremos la clase pipeline de la librería Transformers.
from transformers import pipeline
import numpy as np
import torch
device = 0 if torch.cuda.is_available() else -1
classifier = pipeline(
"audio-classification", model="ntu-spml/distilhubert",
batch_size=16,
device=device,
)
config.json: 0.00B [00:00, ?B/s]
model.safetensors: 0%| | 0.00/94.0M [00:00<?, ?B/s]
Some weights of HubertForSequenceClassification were not initialized from the model checkpoint at ntu-spml/distilhubert and are newly initialized: ['classifier.bias', 'classifier.weight', 'projector.bias', 'projector.weight'] You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.
preprocessor_config.json: 0%| | 0.00/214 [00:00<?, ?B/s]
Device set to use cuda
classifier(np.array(gtzan['train'][0]['array'], dtype="float32"))
[{'score': 0.5040469765663147, 'label': 'LABEL_0'},
{'score': 0.4959530234336853, 'label': 'LABEL_1'}] Calcularemos la precisión del modelo sin entrenamiento. Para esto usaremos el conjunto de datos de prueba.
Lo primero que haremos es calcular las predicciones del modelo para cada muestra del conjunto de datos de prueba.
A continuación, mostraremos las predicciones del modelo para la primera muestra del conjunto de datos de prueba.
predictions = [classifier(np.array(sample['array'], dtype="float32")) for sample in gtzan['test']]
predictions[0]
[{'score': 0.5014662742614746, 'label': 'LABEL_1'},
{'score': 0.4985337555408478, 'label': 'LABEL_0'}] Una vez que tengamos las predicciones del modelo, las compararemos con las etiquetas reales para calcular la precisión del modelo.
A continuación, mostraremos la precisión del modelo.
from sklearn.metrics import accuracy_score
y_true = [f"LABEL_{sample['genre']}" for sample in gtzan['test']]
y_pred = [prediction[0]['label'] for prediction in predictions]
accuracy_score(y_true, y_pred)
0.11
Entrenamiento del modelo¶
Como podemos ver, el modelo sin entrenamiento tiene una precisión del 10%, muy poco. Esto se debe a que el modelo no ha sido entrenado con el conjunto de datos GTZAN.
Para entrenar el modelo usaremos la clase Trainer de la librería Transformers. Esta clase nos permite entrenar modelos de una manera simple y eficiente.
Mientras que con otros modelos necesitamos un Tokenizer en este caso usaremos un feature_extractor. Esta clase nos permitirá procesar muestras de audio para convertirlas en un formato que pueda procesar el modelo.
Entonces crearemos el feature_extractor que usaremos para entrenar el modelo.
from transformers import AutoFeatureExtractor
model_id = "ntu-spml/distilhubert"
feature_extractor = AutoFeatureExtractor.from_pretrained(
model_id, do_normalize=True, return_attention_mask=True
)
A continuación, procesaremos las muestras de audio, lo que las convierte en un formato que puede procesar el modelo. En nuestro caso, reduciremos las muestras de audio a 30 segundos utilizando las opciones max_length y padding del feature_extractor y eliminaremos los datos que no estamos interesados en el conjunto de datos con el método remove_columns.
max_duration = 30.0
def preprocess_function(examples):
audio_arrays = [x for x in examples["array"]]
inputs = feature_extractor(
audio_arrays,
sampling_rate=feature_extractor.sampling_rate,
max_length=int(feature_extractor.sampling_rate * max_duration),
truncation=True,
return_attention_mask=True,
)
return inputs
gtzan_encoded = gtzan.map(
preprocess_function,
remove_columns=["array", "sampling_rate", "file"],
batched=True,
batch_size=16,
num_proc=1,
)
gtzan_encoded
DatasetDict({
train: Dataset({
features: ['genre', 'input_values', 'attention_mask'],
num_rows: 899
})
test: Dataset({
features: ['genre', 'input_values', 'attention_mask'],
num_rows: 100
})
}) Renombramos la columna genre a label para que el Trainer pueda identificarlo como una columna de etiquetas.
gtzan_encoded = gtzan_encoded.rename_column("genre", "label")
Por último, antes de comenzar a entrenar el modelo, crearemos un diccionario en la correspondencia entre los nombres de los géneros y sus valores enteros, de modo que el Trainer pueda identificarlos y permitir un cambio rápido entre los dos formatos.
id2label = {
i: int2str(i)
for i in range(len(gtzan_encoded["train"].features["label"].names))
}
label2id = {v: k for k, v in id2label.items()}
id2label[7]
Entrenamiento del modelo¶
A continuación, crearemos el modelo que entrenaremos.
!!! Este entrenamiento tarda bastante en CPU (varias horas con las 899 muestras y 3 épocas). Si no tienes GPU a mano, usa un entorno de Colab con GPU (Entorno de ejecución → Cambiar tipo de entorno de ejecución → GPU), o salta directamente a la sección Uso del modelo entrenado más abajo y usa la alternativa ya entrenada (ihanif/distilhubert-music-gtzan-classification).
from transformers import AutoModelForAudioClassification
model = AutoModelForAudioClassification.from_pretrained(
model_id, num_labels=len(id2label), id2label=id2label, label2id=label2id
)
Entonces crearemos el TrainerArguments, que nos permitirá configurar el Trainer para entrenar el modelo.
from transformers import TrainingArguments
model_name = model_id.split("/")[-1]
batch_size = 8
gradient_accumulation_steps = 1
num_train_epochs = 3
training_args = TrainingArguments(
f"{model_name}-finetuned-gtzan",
eval_strategy="epoch",
save_strategy="epoch",
learning_rate=5e-5,
per_device_train_batch_size=batch_size,
gradient_accumulation_steps=gradient_accumulation_steps,
per_device_eval_batch_size=batch_size,
num_train_epochs=num_train_epochs,
warmup_ratio=0.1,
logging_steps=5,
load_best_model_at_end=True,
metric_for_best_model="accuracy",
fp16=True
)
Entonces crearemos el Trainer, clase que estará a cargo de entrenar al modelo
import evaluate
import numpy as np
metric = evaluate.load("accuracy")
def compute_metrics(eval_pred):
"""Computes accuracy on a batch of predictions"""
predictions = np.argmax(eval_pred.predictions, axis=1)
return metric.compute(predictions=predictions, references=eval_pred.label_ids)
from transformers import Trainer
trainer = Trainer(
model,
training_args,
train_dataset=gtzan_encoded["train"],
eval_dataset=gtzan_encoded["test"],
processing_class=feature_extractor,
compute_metrics=compute_metrics,
)
trainer.train()
trainer.evaluate()
Uso del modelo entrenado¶
Lo primero que haremos es crear la pipeline con el modelo que hemos entrenado para clasificar los géneros musicales.
music_classifier = pipeline(
"audio-classification",
model=model,
feature_extractor=feature_extractor,
batch_size=16,
device=device,
)
Como alternativa, podemos usar un modelo similar al nuestro, ya preentrenado
music_classifier = pipeline(
"audio-classification",
model="ihanif/distilhubert-music-gtzan-classification",
batch_size=16,
device=device,
)
Clasificar canciones¶
Una vez creada la tubería, podemos usarla para clasificar una canción: recibe el audio (como matriz numpy) y devuelve el género.
Si quieres explorar más música para probar por tu cuenta, aquí tienes enlaces para buscar y descargar (respeta la licencia de cada pista):
Para que el cuaderno funcione sin depender de un archivo que subas tú, la siguiente celda descarga unas cuantas canciones de libre licencia de Wikimedia Commons y las clasifica.
import requests
CABECERAS = {
"User-Agent": "MIA-IA-course-material/1.0 (material docente; sin animo de lucro)"
}
# Canciones de licencia libre en Wikimedia Commons (no todas coinciden con el genero "esperado":
# el propio modelo se equivoca en alguna, lo cual es una buena excusa para comentarlo en clase).
canciones = {
"clasica (Beethoven, Sinfonia 5)": "https://upload.wikimedia.org/wikipedia/commons/5/5b/Ludwig_van_Beethoven_-_Symphonie_5_c-moll_-_1._Allegro_con_brio.ogg",
"country (US Air Force Academy Band)": "https://upload.wikimedia.org/wikipedia/commons/b/b0/Free_Man_-_Wild_Blue_Country_-_United_States_Air_Force_Academy_Band.mp3",
"reggae (US Air Force Band of the West)": "https://upload.wikimedia.org/wikipedia/commons/b/b9/Say_No_-_Top_Flight_-_United_States_Air_Force_Band_of_the_West.mp3",
"blues (US Air Force Band of Mid-America)": "https://upload.wikimedia.org/wikipedia/commons/8/86/St._Louis_Blues_March_-_Shades_of_Blue_-_United_States_Air_Force_Band_of_Mid-America.mp3",
"rock (US Air Force Band of Flight)": "https://upload.wikimedia.org/wikipedia/commons/7/78/4th_Street_Exit_-_Systems_Go_-_United_States_Air_Force_Band_of_Flight.mp3",
}
def cargar_cancion(url, duracion=30, intentos=3):
"""Descarga una URL de audio y devuelve ~duracion segundos del centro, a 16 kHz.
Wikimedia limita las descargas seguidas (HTTP 429): si pasa, esperamos un poco y
reintentamos.
"""
import time
for intento in range(intentos):
respuesta = requests.get(url, headers=CABECERAS, timeout=30)
if respuesta.status_code == 429 and intento < intentos - 1:
time.sleep(10 * (intento + 1))
continue
respuesta.raise_for_status()
break
datos, sr = sf.read(io.BytesIO(respuesta.content))
if datos.ndim > 1:
datos = datos.mean(axis=1)
datos = datos.astype("float32")
duracion_total = len(datos) / sr
inicio = max(0, int((duracion_total / 2 - duracion / 2) * sr))
fragmento = datos[inicio:inicio + int(duracion * sr)]
if sr != TARGET_SR:
fragmento = librosa.resample(fragmento, orig_sr=sr, target_sr=TARGET_SR)
return fragmento
# Si quieres probar con tu propia cancion (un mp3/ogg/wav local), descomenta y adapta:
# datos, sr = librosa.load("mi_cancion.mp3", sr=16000)
# canciones["mi cancion"] = datos # ya en 16 kHz, no hace falta pasar por cargar_cancion
for nombre, valor in canciones.items():
try:
audio = cargar_cancion(valor) if isinstance(valor, str) else valor
prediccion = music_classifier(audio)
print(f"{nombre}: {prediccion[0]['label']} ({prediccion[0]['score']:.2f})")
except requests.exceptions.RequestException as error:
print(f"{nombre}: descarga fallida, omitida ({error})")
clasica (Beethoven, Sinfonia 5): classical (0.99) country (US Air Force Academy Band): country (0.96) reggae (US Air Force Band of the West): reggae (0.90) blues (US Air Force Band of Mid-America): jazz (0.80) rock (US Air Force Band of Flight): jazz (0.97)
Alternativa: clasifica tus propias canciones desde Colab¶
Si las descargas de Wikimedia fallan (limite de peticiones, codigo 429) o simplemente quieres probar con tu propia musica, puedes subirla directamente a Colab en vez de depender de una URL externa:
- En el panel izquierdo de Colab, abre la pestana de archivos (icono de carpeta).
- Crea una carpeta llamada
SAMPLES. - Sube ahi 3-5 canciones tuyas (
.mp3,.wav,.oggo.flac). - Ejecuta la siguiente celda: buscara y clasificara todo lo que encuentre en
SAMPLES/.
from pathlib import Path
def cargar_archivo_local(ruta, duracion=30):
"""Lee un audio local, lo pasa a mono y recorta unos duracion segundos del centro, a 16 kHz."""
datos, sr = sf.read(ruta)
if datos.ndim > 1:
datos = datos.mean(axis=1)
datos = datos.astype("float32")
duracion_total = len(datos) / sr
inicio = max(0, int((duracion_total / 2 - duracion / 2) * sr))
fragmento = datos[inicio:inicio + int(duracion * sr)]
if sr != TARGET_SR:
fragmento = librosa.resample(fragmento, orig_sr=sr, target_sr=TARGET_SR)
return fragmento
SAMPLES_DIR = Path("SAMPLES")
EXTENSIONES = ("*.mp3", "*.wav", "*.ogg", "*.flac")
archivos = sorted(p for patron in EXTENSIONES for p in SAMPLES_DIR.glob(patron)) if SAMPLES_DIR.is_dir() else []
if not archivos:
print(
f"No se encontraron audios en '{SAMPLES_DIR}/'. Crea esa carpeta en el panel de archivos "
"de Colab (icono de carpeta, a la izquierda) y sube ahi 3-5 canciones tuyas (mp3, wav, ogg o flac)."
)
for ruta in archivos:
try:
audio = cargar_archivo_local(ruta)
prediccion = music_classifier(audio)
print(f"{ruta.name}: {prediccion[0]['label']} ({prediccion[0]['score']:.2f})")
except Exception as error:
print(f"{ruta.name}: no se pudo clasificar ({error})")