Архив метки: python

Пример запуска Scrapy отдельным скриптом, без создания проекта

Scrapy представяет собой серъёзный фреймворк для обхода сайтов и извлечения из них структурированной информации. Отличается большим диапазоном решаемых задач (сбор данных, мониторинг, автоматическое тестирование, итд) а также отличной скоростью (благодаря асинхронным вызовам). Использование scrapy подразумевает создание отдельного проекта, со своей заданной структурой и настройкой логики/методов обхода в отдельных python классах scrapy.Spider .
Но что если мы хотим использовать все плюшки большого фремворка, не создавая непосредственно проект? Скажем надо сделать маленькую подзадачу в другом проекте: залогиниться, перейти на заданную страницу и вытащить что-то из html кода?
Столкнувшись с данной задачей мне не удалось быстро найти решение на просторах интернета, поэтому опишу здесь что у меня получилось:

# Устанавливаем библиотеку
pip3 install scrapy

Сценарий следующий:
1. Заходим на стартовую страницу сайта (SCRAPY_START_URL), на которой находится форма входа
2. После успешного залогинивания переходим на вторую страницу с данными SCRAPY_SECOND_URL
3. В функции action из html кода получаем необходимые данные, путём указания пути через аналоги css-селекторов
* Для вызова всего кода используем функцию get_data_by_id, которая по data_id конкретизирует вторую страницу.
* Во время исполнения будет создаваться временный файл items.json, который будет перезаписываться после каждого вызова (он сейчас специально удаляется, если не удалять, то новые результаты будут записываться в конец файла). items.json создается в Scrapy по умолчанию, и как-то без него скорее всего нельзя, но можно задать формат, если вдруг json вам не по душе.
* пример ниже:

import json
import os
import scrapy
from scrapy.crawler import CrawlerProcess, CrawlerRunner
from twisted.internet import reactor
from multiprocessing import Process, Queue


SCRAPY_START_URL = 'https://example.com/'
SCRAPY_USER = 'user_name'
SCRAPY_PASS = 'some_pass'
SCRAPY_SECOND_URL = 'https://example.com/data/'


class CPSpider(scrapy.Spider):
    name = "crawler_name_here"
    start_urls = [SCRAPY_START_URL]
    second_url = None

    def parse(self, response):
        yield scrapy.FormRequest.from_response(
            response,
            formxpath='//form[@id="loginForm"]',
            formdata={
                'auth_signin[username]': SCRAPY_USER,
                'auth_signin[password]': SCRAPY_PASS,
                'Action': '1',
            },
            callback=self.after_login)

    # запускается после успешного логина
    # переходим на странцу, откуда надо стянуть данные
    def after_login(self, response):
        yield scrapy.Request(
            url=CPSpider.second_url,
            callback=self.action)

    # вытаскиваем данные из html кода с помощью xpath
    def action(self, response):
        final_result_txt = response.xpath('//span[@class="data_display"]/b/text()').extract_first(default='some_default')
        print(final_result_txt)

        # создадим словарь и запишем полученный резальтат
        item = dict()
        item['final_result'] = final_result_txt
        return item


def get_data_by_id(data_id):
    if os.path.isfile('items.json'):
        os.remove('items.json')

    CPSpider.reseller_url = SCRAPY_SECOND_URL + str(data_id)

    # а вот здесь хитрая обертка
    # благодаря таким фокусам можно не запускать основное приложение
    def run_spider(spider):
        def f(q):
            try:
                runner = CrawlerRunner(settings={
                    # установим уровень ошибок выше дефолта
                    # чтобы не сыпалось много отладочного вывода
                    # если надо отдебажть, то ставим DEBUG
                    'LOG_LEVEL': 'ERROR',
                    'FEED_FORMAT': 'json',
                    'FEED_URI': 'items.json'
                })
                deferred = runner.crawl(spider)
                deferred.addBoth(lambda _: reactor.stop())
                reactor.run()
                q.put(None)
            except Exception as e:
                q.put(e)

        q = Queue()
        p = Process(target=f, args=(q,))
        p.start()
        result = q.get()
        p.join()

        if result is not None:
            raise result

    try:
        run_spider(CPSpider)
        # считываем результат, полученнный после работы scrapy
        with open('items.json') as json_file:
            res = json.load(json_file)
        if len(res):
            return res[0]['final_result']
    except BaseException as e:
        return 'error'

Сравнение 3ёх фреймворков для машинного обучения: Keras, TensorFlow и PyTorch

Keras, TensorFlow и PyTorch входят в тройку наиболее используемых DL фреймворков. Эти библиотеки используют и как продвинутые дата-инженеры, так и новички в области глубокого обучения. Чтобы облегчить Вам выбор библиотеки, здесь приведем сравнение между троицей Keras, TensorFlow и PyTorch.

Keras — это открытая библиотека для построения нейронных сетей. Написана на Python. По сути это обертка поверх TensorFlow. Идеалогически построена для быстрого прототипирования глубоких нейронных сетей.

Tensorflow — также библиотека с открытым исходным кодом, но благодаря более низкоуровневому доступу позволяет создавать нейронные сети практических любой архитектуры.

Pytorch — основное отличие заключается в том, что разработана была в Facebook AI, поэтому имеет несколько другой интерфейс. Однако многим разработчикам нравится именно библиотека, хоть она и слабее распространена

Параметры сравнения библиотек
* простота использования
* скорость работы
* архитектура
* легкость отладки
* наличие подготовленных наборов данных
* популярность

1.простота использования
Keras — обладает наиболее высокоуровневым API, позволяет работать поверх TensorFlow, CNTK и Theano. Завоевал популярность благодаря синтаксической простоте, способствуя быстрой разработке.
TensorFlow — является одноверменно и высокоуровневой библиотекой так и позволяет вносить изменения в нейронную сейть на весьма низком уровне.
Pytorch — предоставляет низкоуровневый API, ориентированный непосредственно на работу с массивами матриц. Он приобрел огромный интерес в академической среде и в научных исследованиях по глубокому обучению.

2. Скорость/производительность
Производительность в Keras немного ниже чем в Tensorflow и PyTorch, которые обеспечивают
примерно одинаковую скорость, подходящую для серъёзных нагруженных проектов.

3. архитектура
В Keras реализована относительно простая архитектуру, является более читабельной и краткой. С этой стороны, Tensorflow более сложен, хоть Keras и базируется на нем. PyTorch же имеет сложную архитектуру и читаемость кода на нем меньше в сравнении с Keras

4. легкость отладки
В Keras обычно очень редко возникает необходимость отладки простых сетей. Большой набор примеров и армия разработчиков на Keras позволит быстро локализовать ошибку.
В Tensorflow отладка наверняка займет больше времени.
Pytorch обладает хорошим набором инструментов для отладки по сравнению с двумя другими.

5. Работа с датасетами
Keras обычно используется для небольших наборов данных, поскольку он несколько медленнее. А вот TensorFlow и PyTorch используются для высокопроизводительных моделей и большие датасеты не должны вызвать затруднения.

6. популярность
В связи с растущим спросом на технологию глубокого машинного обучения эти три фреймворка приобрели огромную популярность среди python разработчиков. Согласно гугл-трендам, Keras возглавляет список и становится самым популярным, за ним следуют TensorFlow и PyTorch с некоторым отставанием.

Подведем итоги
Для этого рассмотрим ситуации, при которых предпочтительнее будет выбрать ту или другую библиотеку.

Keras наиболее подходит когда для Вас важны:
* Быстрое прототипирование
* При малом наборе данных
* Поддержка сообщества

TensorFlow наиболее подходит когда для Вас важны:
* Работа с большими наборами данных
* Высокая производительность
* функциональность

PyTorch наиболее подходит когда для Вас важны:
* гибкость
* малая продолжительность обучения библиотеки
* быстрая отладка

Пример нахождения косинусного расстония между текстами в sklearn

Косинусное расстояние — это распространенная метрика поиска схожих объектов, часто в разряженном пространстве. Поэтому косинусное расстоние часто применяют для сравнения текстов. Ниже приведу код для поиска наиболее похожего текста с помощью встроенных функций sklearn.
Пусть у нас есть большое количество текстов, предобработаем тексты с помощью tf-idf, после этого возьмем новый текст и найдем 3 самых похожих.

import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import linear_kernel

# зададим массив текстов
some_texts = [
   'текст номер один',
   'текст следующий под номером два',
   'третий набор слов',
   'что-то ещё для полноценного дата-сета',
   'пример текстового документа',
]
df = pd.DataFrame({'texts': some_texts})

# А к данному тексту будем искать наиболее похожий из заданного выше набора
find_nearest_to = "текст номер три"

# формирование весов tf-idf
tfidf = TfidfVectorizer()
mx_tf = tfidf.fit_transform(some_texts)
new_entry = tfidf.transform([find_nearest_to])

# расчет косинусного расстояния
cosine_similarities = linear_kernel(new_entry, mx_tf).flatten()

# запишем все попарные результаты сравнений
df['cos_similarities'] = cosine_similarities
# и отсортируем по убыванию (т.к. cos(0)=1)
df = df.sort_values(by=['cos_similarities'], ascending=[0])

# Выведем 3 самых близких результата
for index, row in df[0:3].iterrows():
    print(row['texts'], row['cos_similarities'])
# output:
# текст номер один 0.7891589913464455
# текст следующий под номером два 0.23490553492076713
# третий набор слов 0.0

Заметим, что в данном примере мы не использовали cosine_similarity, а почему-то взяли linear_kernel. Дело в том, что когда вектора нормализованы (а они нормализованы после TfidfVectorizer) результат будет одинаковый, однако linear_kernel требует меньше вычислительной мощности и вычислится быстрее. Особенно это будет хорошо заметно на больших данных. Если же вектора не нормализованы, то правильно использовать sklearn.metrics.pairwise.cosine_similarity .

Пример также можно рассматривать как рекомендательную систему на большом корпусе каких-то объектов, работает быстро и хорошо подходит для бейзлайна. В случае с объектами не текстовыми, рекомендуется также попробовать использовать CountVectorizer, вместо TfidfVectorizer.

Пример классификации текстов на python с помощью fasttext

Fasttext это библиотека от фейсбука для создания векторного представления слов а также для классификации небольших текстов, а точнее — предложений. Основное отличие от самого известного векторного представления word2vec заключается в том, что каждое слово дополнительно дробится на символьные n-граммы, что позволяет решить проблему редких слов. Достоинством данного эмбеддинга также является высокая скорость работы и оптимизация под небольшие ресурсы (специально была оптимизирована под мобильные устройства).

Здесь мы будем использовать напрямую библиотеку fasttext, однако если Вы продвинутый инженер, то возможно лучше взять обертку gensim, т.к. мне показалось что с ней работа будет более гибкой.

Для установки воспользуемся командой:

pip3 install fasttext

Непосредственно пример:

import pandas as pd
import fasttext


# зададим набор текстов и меток классов
some_texts = [
   'текст номер один',
   'текст следующий под номером два',
   'третий набор слов',
   'что-то ещё для полноценного дата-сета',
   'пример текстового документа',
]
some_labels = [1, 1, 0, 0, 1]

# А теперь одно из разачарований имплементации именно этой библиотеки:
# Для обучения придется сделать файл, где целевой класс должен начинаться с __label__
df_train = pd.DataFrame({'txt': some_texts, 'target': some_labels})
df_train['target'] = df_train['target'].apply(lambda x: '__label__' + str(x))
df_train[['target', 'txt']].to_csv('train_data.txt', header=False, index=False, sep="\t")

# обучаем на 20 эпохах, полный набор гиперпараметров можно взглянуть на официальном сайте fasttext.cc
model = fasttext.train_supervised(input='train_data.txt', epoch=20)

# указываем чтобы отдавало вероятности обоих классов, по умолчанию k=1
p = model.predict('текст номер четыре', k=2)

print(p)
# (('__label__1', '__label__0'), array([0.5010367 , 0.49898329]))

Второе разачарование заключается в том, что в итоговом предсказании выдаётся сначала самый вероятный класс, и если требуется получить вероятность по конкретному классу, то нужно сначала узнать индекс нужного класса и только после этого по индексу взять вероятность. Для 2ух классов можно сделать примерно так:

    if p[0][0] == '__label__0':
        real_probability = p[1][1]
    else:
        real_probability = p[1][0]

В целом данный пример предлагается использовать для быстрого получения результата в задаче классификации текстов. Я попробовал код выше на соревновании mlbootcamp . ru по классификации экспертных вопросов и он показал результат из коробки лучше чем tf-idf: 0.73 fasttext против 0.68 tf-idf + lgbm

Группировка (groupby) в pandas c сохранением количества строк, генерация агрегатных фич.

Пусть у нас есть датафрейм и нам надо сгруппировать его по колонке «A»

import pandas as pd
import numpy as np


df = pd.DataFrame()
df['A'] = [1, 1, 1, 1, 1, 2, 2, 3, 3, 3]
df['v'] = [3, 5, 7, 10, 20, 11, 1, 3, 8, 10]

print(df.head(10))
# распечатаем датафрейм для наглядности
   A   v
0  1   3
1  1   5
2  1   7
3  1  10
4  1  20
5  2  11
6  2   1
7  3   3
8  3   8
9  3  10

Для группировка выберем функцию среднего и после применения операции группировки в DataFrame остается всего 2 колонки. Пример:

res = df.groupby(by=['A'], as_index=False)['v'].mean()
print(res.head())
# результат
   A  v
0  1  9
1  2  6
2  3  7

Как сделать так, чтобы просто добавить результат агрегации в новую колонку? Для этого воспользуемся функцией transform

df['col_mean'] = df.groupby(by=['A'], as_index=False)['v'].transform(lambda s: np.mean(s.values))
print(df.head(10))
# получаем результат агрегации в новой колонке
   A   v  col_mean
0  1   3       9.0
1  1   5       9.0
2  1   7       9.0
3  1  10       9.0
4  1  20       9.0
5  2  11       6.0
6  2   1       6.0
7  3   3       7.0
8  3   8       7.0
9  3  10       7.0

В соревнованиях по машинному обучению часто приходится иметь дело с категориальными признаками, и добавление агрегатных фич обычно сразу улучшает результат. Ниже приведу пример функции, в которой применяются наиболее распространенные функции: минимум, максимум, сумма, среднее, медиана, частота встречаемости итд.

import pandas as pd
import numpy as np
import scipy
from scipy import stats


def make_agg(df_in, group_col_name, stat_col_name):
    df = df_in.copy()
    # то же самое что и count, количество
    df['col_count'] = df.groupby(by=[group_col_name], as_index=False)[stat_col_name].transform(lambda s: s.values.shape[0])
    # среднее значение по сгруппированному столбцу
    df['col_mean'] = df.groupby(by=[group_col_name], as_index=False)[stat_col_name].transform(lambda s: np.mean(s.values))
    # медиана по сгруппированному столбцу
    df['col_median'] = df.groupby(by=[group_col_name], as_index=False)[stat_col_name].transform(lambda s: np.median(s.values))
    # сумма по сгруппированному столбцу
    df['col_sum'] = df.groupby(by=[group_col_name], as_index=False)[stat_col_name].transform(lambda s: np.sum(s.values))
    # минимум и максимум по сгруппированному столбцу
    df['col_max'] = df.groupby(by=[group_col_name], as_index=False)[stat_col_name].transform(lambda s: np.max(s.values))
    df['col_min'] = df.groupby(by=[group_col_name], as_index=False)[stat_col_name].transform(lambda s: np.min(s.values))
    # стандартное отклонение по сгруппированному столбцу
    df['col_std'] = df.groupby(by=[group_col_name], as_index=False)[stat_col_name].transform(lambda s: np.std(s.values))
    # квантили в 20 и 80 % по сгруппированному столбцу
    df['col_q20'] = df.groupby(by=[group_col_name], as_index=False)[stat_col_name].transform(lambda s: np.quantile(s.values, q=0.2))
    df['col_q80'] = df.groupby(by=[group_col_name], as_index=False)[stat_col_name].transform(lambda s: np.quantile(s.values, q=0.8))
    # skew
    df['col_skew'] = df.groupby(by=[group_col_name], as_index=False)[stat_col_name].transform(lambda s: scipy.stats.skew(s))

    return df

res = make_agg(df, 'A', 'v')
print(res.head(10))

Как видите в transform можно написать любую свою lambda функцию. Только помните, что переменная придет типом Series, и для того чтобы случайно не запутаться в индексах, рекомендую добавлять values, тем самым переходя к стандартному numpy массиву. Также считать агрегатные статистики лучше сразу по объединенному train+test датасету, а не по отдельности для train и test. Так как объединенный датасет будет более показательным.

Две функции повышения точности классификатора с помощью target encoding / mean encoding

Раз Вы смогли нагуглить эту статью, значит скорее всего знаете что такое target encoding, а тажке ознакомлены со всеми прелестями этого мощного метода и знаете все опасности переобучения, которые могут возникнуть при неправильном использовании подхода.
Поэтому здесь я приведу лишь две функции, которые можно быстро использовать для Ваших задач.
Если же не знакомы c mean encoding, то вкратце так:
На основе знаний о target, и его распределения по какой-нибудь категориальной фичи можно добавить новую переменную, показывающую характеристику встречаемости чисто внутри какой-то категории. Для примера пусть есть датасет с разными характеристиками автомобиля, необходимо предсказать город, где был куплен автомобиль. Можно подсчитать среднее кол-во каждой марки в каждом городе и добавить это как дополнительную фичу. Так скажем для тойоты это число будет больше во Владивостоке, чем для Москвы и наоборот, какой-нибудь мерседес явно будет более встречаемый в Москве, нежели рядом с Японией. Как видим, разделяемость по новой фиче будет очень хорошая. Но в этом и есть большой риск переобучения (например какая-нибудь редкая марка, которая в датасете встретится всего пару раз и в одном городе) поэтому напрямую считать среднее не рекомендуется, а правильнее будет использовать регуляризации. Ниже приведены две функции для корректного использования mean-encoding:

Функция 1: CV loop регуляризация
Достаточно интуитивный и устойчивый метод для mean encoding. Данные разбиваются на фолды и значение для каждого фолда вычисляется на основе оставшихся фолдов. Таким образом не будет переобучения на текущем куске данных, т.к. вычисляемое среднее значение будет браться на остальном подмножестве данных. Обычно 4-5 фолдов достаточно.

# Для начала создадим дата фрейм с категориальной фичей
import numpy as np
import pandas as pd
from sklearn.model_selection import StratifiedKFold


cities = ['C1', 'C1', 'C1', 'C2', 'C2', 'C3', 'C3', 'C3', 'C3', 'C3']
target = [1, 1, 0, 1, 0, 1, 1, 0, 0, 0]

df = pd.DataFrame()
df['cities'] = cities
df['target'] = target

def make_mean_encoding(df_tr, target_col_name, feature_col_name):
    y_tr = df_tr[target_col_name].values
    skf = StratifiedKFold(n_splits=2, shuffle=True, random_state=123)
    train_new = df_tr.copy()

    global_mean = df_tr[target_col_name].mean()
    train_new[feature_col_name + '_mean_target'] = global_mean

    for tr_ind, val_ind in skf.split(y_tr, y_tr):
        X_tr, X_val = df_tr.iloc[tr_ind], df_tr.iloc[val_ind]
        for col in [feature_col_name]:
            means = X_val[col].map(X_tr.groupby(col)[target_col_name].mean())
            X_val[col + '_mean_target'] = means
        train_new.iloc[val_ind] = X_val

    # fill nan
    train_new.fillna(global_mean, inplace=True)

    return train_new

df_encoded = make_mean_encoding(df, 'target', 'cities')
print(df_encoded.head(10))

Функция 2: Регуляризация на основе размытия
Суть данной регуляризации в том, что мы доверяем большим категориям, с большим кол-вом значений и устанавливаем коэффициент недоверия к небольшим, слабопредставленным категориям. По сути говоря коэффициент alpha это размер группы, начиная с которой мы доверяем среднему значению

def make_mean_encoding_smooth(df_tr, target_col_name, feature_col_name):
    train_new = df_tr.copy()
    global_mean = df_tr['target'].mean()
    encod_type = df_tr.groupby(by=[feature_col_name], as_index=False)[target_col_name].transform(np.mean).values
    nrows = df_tr.groupby(by=[feature_col_name], as_index=False)[target_col_name].transform(len).values
    alpha = 4
    train_new[feature_col_name + '_mean_target'] = (encod_type * nrows + global_mean * alpha) / (nrows + alpha)
    return train_new

df_encoded = make_mean_encoding_smooth(df, 'target', 'cities')
print(df_encoded.head(10))

Некоторые библиотеки градиентного бустинга из коробки умеют делать mean-encoding, например catboost. Делает котяра эту вещь отменно, достаточно просто указать категориальные фичи, и мне даже когда-то не хотелось лезть в эту тему, но в один момент надо было работать с очень большим датасетом, и оказалась что эта фича библиотеки ужасно прожорлива на используемую память, а если использовать ещё и обучение на видеокарте, то терабайтов оперативной памяти не хватит. Поэтому иногда полезно самим прикрутить фичи на основе mean-енкодинга, кроме того, за основу не обязательно брать таргет, можно поэкспериментировать и с другими величинами из датасета.

Классификация аудио / звуков с помощью 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()

Наложение текста на изображение с помощью python

В основном, для базовых операций с изображениями в питоне, используются две библиотеки OpenCV и PIL. Одна из операций — это добавление произвольного текста в определенную область на фото. В данном посте рассмотрим оба варианта добавления текста и с помощью OpenCV и через PIL. Однако, забегая вперёд скажу, что в OpenCV весьма урезанная функция добавления текста.

1. Пример наложения текста с использованием библиотеки python-OpenCV

import numpy as np
import cv2

# создадим белое изображение
# или можно считать изобрежние с помощью cv2.imread("path_to_file")
img = np.zeros((256, 512, 3), np.uint8)
img[:, :, :] = 255

font = cv2.FONT_HERSHEY_COMPLEX
# вставка текста красного цвета
cv2.putText(img, 'наш произвольный текст', (10, 150), font, 1, color=(0, 0, 255), thickness=2)

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

# есть ограниченное кол-во вариантов выбора шрифта
# FONT_HERSHEY_COMPLEX
# FONT_HERSHEY_COMPLEX_SMALL
# FONT_HERSHEY_DUPLEX
# FONT_HERSHEY_PLAIN
# FONT_HERSHEY_SCRIPT_COMPLEX
# FONT_HERSHEY_SCRIPT_SIMPLEX
# FONT_HERSHEY_SIMPLEX
# FONT_HERSHEY_TRIPLEX
# FONT_ITALIC

Одним из основных минусов первого варианта — невозможность выбрать нужный шрифт. Поэтому советую данный способ для быстрого прототипирования, когда надо просто как-то подписать изображение, например вывести время в последовательности кадров на видео или просто вывести техническую информацию.

Для более продвинутого добавления надписей (можно сказать даже художественного) давайте сформулируем требования, предъявляемые к скрипту.
1. Использование произвольного шрифта из файла ttf
2. Возможность задать угол наклона текста
3. Возможность задать прозрачность текста
4. Выравнивание текста по центру
Четвёртый пункт очень важен, ведь используя произвольный шрифт мы не можем расчитать ширину надписи, и было бы здорово передать эту предобработку библиотеке.
Теперь перейдём к коду:

2. Пример наложение текста с использованием библиотеки PIL

import numpy as np
import cv2
from PIL import Image, ImageDraw, ImageFont

# создадим белое изображение
# или можно считать изобрежние с помощью cv2.imread("path_to_file")
img = np.zeros((256, 512, 3), np.uint8)
img[:, :, :] = 255


# для простоты и совместимости возьмем пустое изображение из первого примера
# Чтобы не использовать opencv, а только PIL используйте функцию Image.open()
def put_text_pil(img: np.array, txt: str):
    im = Image.fromarray(img)

    font_size = 24
    font = ImageFont.truetype('LibreFranklin-ExtraBold.ttf', size=font_size)

    draw = ImageDraw.Draw(im)
    # здесь узнаем размеры сгенерированного блока текста
    w, h = draw.textsize(txt, font=font)

    y_pos = 50
    im = Image.fromarray(img)
    draw = ImageDraw.Draw(im)

    # теперь можно центрировать текст
    draw.text((int((img.shape[1] - w)/2), y_pos), txt, fill='rgb(0, 0, 0)', font=font)

    img = np.asarray(im)

    return img


img = put_text_pil(img, 'Some Styled Black Text Here')
cv2.imshow('Result', img)
cv2.waitKey()

Пример кода с альфа каналом и наклоном будет чуть позже