Python. Cжимаем звуковую дорожку при помощи алгоритма PCA

Python. Cжимаем звуковую дорожку при помощи алгоритма PCA

Библиотеки и данные

# Понадобится для чтения и записи аудио файлов
from scipy.io import wavfile

# Это нужно для проигрывания аудио файлов прямо в блокноте
from IPython.display import Audio

# А это набор для задачи
import numpy as np
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA
# Скачаем данные
! wget https://www.dropbox.com/s/p5147nr8mzemxnr/Beethoven_Violin_Sonata_Op_96_first_movement_bars_1-22.wav
# Прочитаем аудио дорожку при помощи wavfile
samplerate, data = wavfile.read('Beethoven_Violin_Sonata_Op_96_first_movement_bars_1-22.wav')

Анализ данных

Samplerate есть частота дискретизации — стандартное для аудио значение — 44100 Гц.
Частота дискретизации говорит о том, сколько последовательных элементов массива с сигналом кодируют звук длительностью 1 секунда.

# Посмотрим какая у нас частота дискретизации
print(samplerate)

Вывод: 44100
# Если поделить длину массива сигнала на samplerate, получится длительность аудиодорожки в секундах

print(len(data) / samplerate)
# 45 секунд - похоже на правду:)

Вывод:45.139591836734695
# Заметим, что звук - стерео, так как сигнал кодируется двумя каналами (для левого и правого динамика):

print(data.shape)

Вывод:(1990656, 2)
# Отрисуем сигналы в обоих каналах
# Канал 1
plt.figure(figsize=(20,5))
plt.plot(data[:,0])
plt.show()

# Канал 2
plt.figure(figsize=(20,5))
plt.plot(data[:,1])
plt.show()
Вывод:
График частоты аудио от времени
График частоты аудио от времени
# Усредним каналы, и получим моно звук, с которым будет проще работать
mono_sound = np.mean(data, axis=1)
print(mono_sound.shape)

Вывод:(1990656,)
# Давайте послушаем наконец, что же мы будем сжимать:)
Audio(mono_sound, rate = samplerate)
# Для удобства - обрежем массив с сигналом так, чтобы его было удобнее делить на равные части, датасет из которых и необходимо будет сжать известными методами.
mono_sound_to_cut = mono_sound[:1990000]
# Проверим, что наш звук теперь это просто вектор чисел
print(mono_sound_to_cut.shape)

Вывод: (1990000,)

Разделим сигнал на равные части (длину каждой части возьмем равной 1000) и соберем из них «датасет», который будет представлять из себя двухмерный массив — «матрицу», в которой каждая часть сигнала длины 1000 находится в отдельной строке).

То есть первая часть содержит первые 1000 чисел сигнала и находится в первой строке матрицы, вторая часть следующие 1000 чисел сигнала и находится во второй строке матрицы и так далее.

mono_sound_to_cut = np.reshape(mono_sound_to_cut, (1990, 1000))
mono_sound_to_cut

Вывод:
array([[  0. ,  -1. ,  -1. , ...,  88.5,  58.5,  18. ],
       [-25.5, -61.5, -81.5, ...,  30.5, -20.5, -53. ],
       [-67.5, -65.5, -50. , ..., 118.5, 118.5,  86.5],
       ...,
Переводем "матрицу" обратно в звуковой сигнал, то есть развернем данные обратно из матрицы размера (число объектов, 1000) в вектор длины (число объектов * 1000). Проверим, что все работает верно, путем воспроизведения "восстановленного" сигнала -- он должен совпасть в точности с изначальным (ведь им на самом деле и является).

Применение алгоритма

Выполним PCA преобразование нашей матрицы, и получим данные, сжатые в пространство меньшей размерности.

На этом этапе у нас есть наш «датасет» с 1000 «признаками» и мы хотим уменьшить число «признаков» путем применения метода PCA. Число компонент  для начала не стоит брать слишком маленькое число, чтобы потом было проще понять, в случае плохого результата — компонент оказалось недостаточно или где-то ошиблись.

from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
print(mono_sound_to_cut.shape)

# Scale data before applying PCA
scaling=StandardScaler()

#Use fit and transform method
scaling.fit(mono_sound_to_cut)
Scaled_data=scaling.transform(mono_sound_to_cut)

# Set the n_components=1000
principal=PCA(n_components=1000)
principal.fit(Scaled_data)
x=principal.transform(Scaled_data)

# Check the dimensions of data after PCA
print(x.shape)

Вывод:
(1990, 1000) 
(1990, 1000)

Построим две главные компоненты в наглядной форме.

То есть необходимо применить метод PCA к нашей матрице с числом компонент, равным 2 (тем самым получив датасет с 2 «признаками»). Это позволит нам отрисовать данные на плоскости, чтобы попытаться уловить зависимости.

print(mono_sound_to_cut.shape)

# Scale data before applying PCA
scaling=StandardScaler()

#Use fit and transform method
scaling.fit(mono_sound_to_cut)
Scaled_data=scaling.transform(mono_sound_to_cut)

# Set the n_components=2
principal=PCA(n_components=2)
principal.fit(Scaled_data)
x=principal.transform(Scaled_data)

# Check the dimensions of data after PCA
print(x.shape)

# Отрисуем
plt.figure(figsize=(20,5))
plt.plot(x[:,0])
plt.show()


Постройте scatter plot датасета в пространстве первых двух компонент, а третью используйте как цвет. То есть необходимо применить метод PCA уже с 3 компонентами и нарисовать опять точки, только передав третью компоненту в качестве цвета.

print(mono_sound_to_cut.shape)

# Scale data before applying PCA
scaling=StandardScaler()

#Use fit and transform method
scaling.fit(mono_sound_to_cut)
Scaled_data=scaling.transform(mono_sound_to_cut)

# Set the n_components=2
principal=PCA(n_components=2)
principal.fit(Scaled_data)
x=principal.transform(Scaled_data)

# Check the dimensions of data after PCA
print(x.shape)
plt.figure(figsize=(15, 5))
plt.scatter(x[:,0],x[:,1], c=(1, 0, 0))

Нам осталось заняться непосредственно «сжатием» звука и проверкой правильности наших действий.

Выполним обратное PCA преобразование сжатых данных и получим «матрицу» с сжатым звуком.

Воспользуемся обратным преобразованием PCA (inverse_transform), чтобы из матрицы размера (число объектов, число компонент) перейти в матрицу размера (число объектов, 1000).

print(x.shape)
new_data = principal.inverse_transform(x)
print(new_data.shape)

Вывод:
(1990, 2) 
(1990, 1000)
# сильно ужатый звук стал шумом
result_restore = np.reshape(new_data, (1990000))
print(result_restore.shape)
Audio(result_restore, rate = samplerate)

Исследуем зависимость качества звука от числа компонент. Подберем «на слух» минимальное число компонент, при котором звук практически не отличается от оригинала.

# Scale data before applying PCA
scaling=StandardScaler()

#Use fit and transform method
scaling.fit(mono_sound_to_cut)
Scaled_data=scaling.transform(mono_sound_to_cut)

# Set the n_components=100
principal=PCA(n_components=100)
principal.fit(Scaled_data)
x=principal.transform(Scaled_data)

# Check the dimensions of data after PCA
print(x.shape)
new_data = principal.inverse_transform(x)
print(new_data.shape)
result_restore = np.reshape(new_data, (1990000))
print(result_restore.shape)
Audio(result_restore, rate = samplerate)
print("Исходное МОНО")
Audio(mono_sound, rate = samplerate)

Попробуем отфильтровать сигнал с помощью функции gaussian_filter1d из scipy.ndimage. Это поможет убрать неприятный дробовой шум при сильном сжатии

from scipy.ndimage import gaussian_filter1d
Audio(gaussian_filter1d(result_restore, 2), rate = samplerate)

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *