Архив за месяц: Август 2019

Классификация аудио / звуков с помощью python

В данном посте приведу относительно простой способ классифицировать звукозаписи с помощью python. Как и для любой задачи классификации, в первую очередь понадобится из аудиозаписи извлечь фичи, для этого воспользуемся библиотекой librosa. Звук — весьма сложная структура, не спроста задача распознавания голоса решена по сути только гигантами вроде гугла, яндекса, да и то не идеально. Хоти они явно не ограничены в вычислительных ресурсах.
В общем примерная суть всех фич заключается в том, что звук — это спектрограммы, т.е. если все данные закидывать как фичи, то классификатору будет очень тяжело. Поэтому мы из различных спектрограмм будем брать только некоторые характеристики в виде среднего, дисперсии, минимальных, максимальных значений итд.

Подробнее о каждом спектре можно прочитать здесь: github.com/subho406/Audio-Feature-Extraction-using-Librosa/blob/master/Song%20Analysis.ipynb
Здесь же представлю упрощённую версию:

import librosa
import numpy as np
import librosa.display
import scipy
from scipy import stats

# функция вернет 72 фичи для аудиозаписи по пути wav_path
def get_base_features(wav_path):
    ff_list = []

    y, sr = librosa.load(wav_path, sr=None)

    y_harmonic, y_percussive = librosa.effects.hpss(y)

    tempo, beat_frames = librosa.beat.beat_track(y=y_harmonic, sr=sr)
    chroma = librosa.feature.chroma_cens(y=y_harmonic, sr=sr)
    mfccs = librosa.feature.mfcc(y=y_harmonic, sr=sr, n_mfcc=13)
    cent = librosa.feature.spectral_centroid(y=y, sr=sr)
    contrast = librosa.feature.spectral_contrast(y=y_harmonic, sr=sr)
    rolloff = librosa.feature.spectral_rolloff(y=y, sr=sr)
    zrate = librosa.feature.zero_crossing_rate(y_harmonic)

    chroma_mean = np.mean(chroma, axis=1)
    chroma_std = np.std(chroma, axis=1)

    for i in range(0, 12):
        ff_list.append(chroma_mean[i])
    for i in range(0, 12):
        ff_list.append(chroma_std[i])

    mfccs_mean = np.mean(mfccs, axis=1)
    mfccs_std = np.std(mfccs, axis=1)

    for i in range(0, 13):
        ff_list.append(mfccs_mean[i])
    for i in range(0, 13):
        ff_list.append(mfccs_std[i])

    cent_mean = np.mean(cent)
    cent_std = np.std(cent)
    cent_skew = scipy.stats.skew(cent, axis=1)[0]

    contrast_mean = np.mean(contrast,axis=1)
    contrast_std = np.std(contrast,axis=1)

    rolloff_mean=np.mean(rolloff)
    rolloff_std=np.std(rolloff)

    data = np.concatenate(([cent_mean, cent_std, cent_skew], 
                           contrast_mean, contrast_std, 
                           [rolloff_mean, rolloff_std, rolloff_std]), axis=0)
    ff_list += list(data)

    zrate_mean = np.mean(zrate)
    zrate_std = np.std(zrate)
    zrate_skew = scipy.stats.skew(zrate,axis=1)[0]

    ff_list += [zrate_mean, zrate_std, zrate_skew]

    ff_list.append(tempo)

    return ff_list

Далее для классификации можно использовать любой алгоритм градиентного бустинга, буть то lightgbm, xgboost или catboost.
Предположим, что в одной папке лежат все аудиозаписи из одного класса, а в другой папке аудиозаписи из второго класса. Прочитаем их, вычислим фичи и запустим обучение.

import glob
from catboost import CatBoostClassifier
from sklearn.model_selection import train_test_split

h_features = []
s_features = []
ii, jj = 0, 0

for f_path in glob.glob('../Training_Data/human/*.wav'):
    h_features.append(get_base_features(f_path))
    ii += 1
    if ii > 50:
        break
for f_path in glob.glob('../Training_Data/spoof/*.wav'):
    s_features.append(get_base_features(f_path))
    jj += 1
    if jj > 50:
        break


X_train, X_test, y_train, y_test = train_test_split(
                                        h_features+s_features,
                                        [1]*len(h_features) + [0]*len(s_features),
                                        test_size=0.25,
                                        random_state=42,
                                    )

clf = CatBoostClassifier(iterations=200)
clf.fit(X_train, y_train, eval_set=(X_test, y_test))

Стоит отметить, что почему-то библиотека librosa медленно читает файл. Поэтому предложу альтернативный метод из библиотеки scipy:

import scipy.io.wavfile
sr, y = wavfile.read("audio/some_file.wav")

Пример KFold из библиотеки sklearn (кроссвалидации по фолдам)

Практически всегда от моделей машинного обучения требуется указать их точность. Для расчета точности необходимо
1) обучить модель на тренировочном датасете
2) предсказать результаты на тестовом датасете
3) сравнить правильные данные с предсказанными
Данная процедура проста, но в зависимости от разбиения на обучающий датасет и тестовый датасет мы получим немного разные значения. И какой же результат более правильный?
Правильного нет, ведь точность классификатора само по себе понятие относительное. Но интуитивно понятно, что для лучшей оценки необходимо провести процедуру 1-2-3 как можно большее количество раз.

И вот как раз для оценки точности классификатора и придумали кросс-валидацию. Или K-fold cross-validation.
Идея проста, весь датасет делится на N частей. На каждой итерации N-1 часть идёт на train, и одна на test.
В sklearn для этого есть специальный метод cross_val_score:

import numpy as np
from sklearn.model_selection import cross_val_score
from sklearn.datasets import load_iris
from sklearn.ensemble import RandomForestClassifier

# воспользуемся известным датасетом для примера
iris = load_iris()

clf = RandomForestClassifier(n_estimators=30, random_state=42)
# передаем классификатор, X, y и кол-во фолдов=5
res = cross_val_score(clf, iris['data'], iris['target'], cv=5)

print(res)
# [0.96666667 0.96666667 0.93333333 0.96666667 1.        ]
print(np.mean(res))
# 0.966

Как видим всё очень просто, по сравнению с train_test_split мы получим более точную оценку качества классификатора, т.к. провели эксперимент N раз (в нашем случае 5 раз).
Да, пришлось потратить в 5 раз больше времени по сравнению с train_test_split, но когда данных не очень много — это оправдано.

Однако не всегда удаётся воспользоваться методом cross_val_score, например если хотим что-то ещё подсчитать в это время.
Для этого есть более гибкий метод KFold.
Kfold часто используют не только для оценки точности классификатора, но и например для контроля переобучения для классификатора.
Для многих моделей очень важно знать, в какой момент начинается переобучение. Таким образом можно обучить 10 классификаторов с контролем переобучения и потом усреднить их предсказания. Это может дать дать лучий результат, чем если обучить одну модель сразу на всех данных, без контроля переобучения.
Для примера возьмем библиотеку catboost и будем валидироваться оставшемся фолде

from sklearn.model_selection import KFold
from sklearn.datasets import load_iris
from catboost import CatBoostClassifier

iris = load_iris()

# инициализация KFold с 5тью фолдами
cv = KFold(n_splits=5, random_state=42, shuffle=True)

classifiers = []
# метод split возвращает индексы для объектов train и test
for train_index, test_index in cv.split(iris['target']):
    clf = CatBoostClassifier(random_state=42, iterations=200, verbose=50, learning_rate=0.1)
    X_train, X_test = iris['data'][train_index], iris['data'][test_index]
    y_train, y_test = iris['target'][train_index], iris['target'][test_index]
    clf.fit(X_train, y_train, eval_set=(X_test, y_test), use_best_model=True)

    # получим 5 оптимальных классификаторов
    classifiers.append(clf)

Определение угла наклона текста на изображении

В данном посте поделюсь итеративным методом определения угла наклона сканированного текста.
Суть метода заключается в следующем: постепенно меняя угол наклона — считаем некоторую характеристику-функцию от изображения (о ней чуть дальше). При каком угле наклона получим максимум — значит это и есть оптимальный угол наклона.
Теперь о том, что считать, для этого внимательно взглянем на то как выглядит текст на сканированном документе. Если представим изображение только в виде черных и белых пикселей и потом спроецируем их все на ось X, то в местах где есть строчки гистограмма будет выдавать пики по черным пикселям, а где междустрочный интервал — по белым пикселям. Пики будут максимально выраженными при правильном угле. Но мы хотим получить наиболее простой алгоритм, поэтому будет считать только кол-во черных и белых пикселей при проекции на ось X. Т.к. строки текста парралельны друг другу, то при лучшем угле кол-во белых пикселей будет максимальным, а кол-во черных минимальным. Этим и воспользуемся:

import numpy as np
import cv2


def get_angle(img):
    # сперва переведём изображение из RGB в чёрно серый
    # значения пикселей будут от 0 до 255
    img_gray = cv2.cvtColor(img.copy(), cv2.COLOR_BGR2GRAY)

    # а теперь из серых тонов, сделаем изображение бинарным
    th_box = int(img_gray.shape[0] * 0.007) * 2 + 1
    img_bin_ = cv2.adaptiveThreshold(img_gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, th_box, th_box)

    img_bin = img_bin_.copy()
    num_rows, num_cols = img_bin.shape[:2]

    best_zero, best_angle = None, 0
    # итеративно поворачиваем изображение на пол градуса
    for my_angle in range(-20, 21, 1):
        rotation_matrix = cv2.getRotationMatrix2D((num_cols/2, num_rows /2 ), my_angle/2, 1)
        img_rotation = cv2.warpAffine(img_bin, rotation_matrix, (num_cols*2, num_rows*2),
                                      borderMode=cv2.BORDER_CONSTANT,
                                      borderValue=255)

        img_01 = np.where(img_rotation > 127, 0, 1)
        sum_y = np.sum(img_01, axis=1)
        th_ = int(img_bin_.shape[0]*0.005)
        sum_y = np.where(sum_y < th_, 0, sum_y)

        num_zeros = sum_y.shape[0] - np.count_nonzero(sum_y)

        if best_zero is None:
            best_zero = num_zeros
            best_angle = my_angle

        # лучший поворот запоминаем
        if num_zeros > best_zero:
            best_zero = num_zeros
            best_angle = my_angle

    return best_angle * 0.5


img = cv2.imread('singapore_01.jpg')
best_angle = get_angle(img)
print(best_angle)

cv2.imshow('Result', img)
cv2.waitKey()