Распознавание речи Часть 1: Создание данных обучения

Вы все слышали о нейронных сетях , нет? Один из «привет мир» нейронных сетей OCR с MNIST dataset . Моя идея для распознавания речи состоит в том, чтобы обучить нейронную сеть с помощью коротких обозначенных спектрограммы вместо символов. К сожалению, я не знаю базы данных для этого, поэтому мне нужно создать собственное программное обеспечение для этого. Любые предложения по его улучшению?

Примечание: В конечном итоге это предназначено для распознавания речи в реальном времени, но при тестировании я не мог строить и писать спектрограммы для записи достаточно быстро, чтобы это произошло (поскольку это мешает тому, как часто CPU может пробовать звук с микрофона). Я был бы признателен за любые решения, детализирующие, как повысить производительность в этой области.


main.py:

from record import AudioHandler
import sys
import shutil
import os

if __name__ == '__main__':    
    invalid = 0
    audio = AudioHandler()
    if os.path.isdir(audio.DATA_DIR):
        shutil.rmtree(audio.DATA_DIR) # clean folder
    while not invalid:
        try:
            invalid = audio.listen()
        except KeyboardInterrupt:
            break

    audio.convert_fileblock()
    audio.save_all_audio()
    sys.exit()

record.py:

import pyaudio
import struct
import math
import time
import numpy as np
import os
import glob
import scipy.io.wavfile
from scipy import signal
import matplotlib.pyplot as plt

def get_rms(block):
    try:
        rms = np.sqrt(np.mean(np.square(np.abs(block))))
    except Exception as e:
        print('RMS error: {}'.format(e))

    return rms

class AudioHandler(object):
    def __init__(self):
        self.DATA_DIR = 'raw_data'
        self.RATE = 16000
        self.INPUT_BLOCK_TIME = 0.03 # 30 ms
        self.CHANNELS = 1
        self.INPUT_FRAMES_PER_BLOCK = int(self.RATE * self.INPUT_BLOCK_TIME)
        self.SENTENCE_DELAY = 1.1 # seconds
        self.MAX_SILENT_BLOCKS = math.ceil(self.SENTENCE_DELAY / self.INPUT_BLOCK_TIME)
        self.THRESHOLD = 40 # dB
        self.pa = pyaudio.PyAudio()
        self.stream = self.open_mic_stream()
        self.save_counter = '0'.zfill(8)
        self.silent_blocks = 0
        self.listening = False
        self.audio = []

    def stop(self):
        self.stream.close()

    def find_input_device(self):
        device_index = None
        for i in range( self.pa.get_device_count() ):
            devinfo = self.pa.get_device_info_by_index(i)

            for keyword in ['mic','input']:
                if keyword in devinfo['name'].lower():
                    print('Found an input: Device {} - {}'.format(i, devinfo['name']))
                    device_index = i
                    return device_index

        if device_index == None:
            print('No preferred input found; using default input device.')

        return device_index

    def open_mic_stream(self):
        device_index = self.find_input_device()

        stream = self.pa.open(  format = pyaudio.paInt16,
                                channels = self.CHANNELS,
                                rate = self.RATE,
                                input = True,
                                input_device_index = device_index,
                                frames_per_buffer = self.INPUT_FRAMES_PER_BLOCK)

        return stream

    def save_block(self, snd_block):
        self.audio.append(snd_block)
        flat_block = np.hstack(snd_block)
        if not os.path.isdir(self.DATA_DIR):
            os.makedirs(self.DATA_DIR)
        np.savetxt('{}/block{}.txt'.format(self.DATA_DIR, self.save_counter), flat_block)
        self.save_counter = str(int(self.save_counter) + 1).zfill(8)

    def listen(self):
        try:
            raw_block = self.stream.read(self.INPUT_FRAMES_PER_BLOCK, False)
            snd_block = np.fromstring(raw_block, dtype=np.int16)
        except Exception as e:
            print('Error recording: {}'.format(e))
            return

        amplitude = get_rms(snd_block)
        if amplitude > self.THRESHOLD:
            self.listening = True
            self.silent_blocks = 0 # reset counter
        else:
            self.silent_blocks += 1

        if self.listening:
            self.save_block(snd_block)
        if self.silent_blocks > self.MAX_SILENT_BLOCKS and self.listening:
            # remove last stored silent blocks
            for i in range(int(self.save_counter) - 1, int(self.save_counter) - self.MAX_SILENT_BLOCKS, -1):
                self.audio.pop()
                i = str(i).zfill(8)
                os.remove('{}/block{}.txt'.format(self.DATA_DIR, i))
            self.listening = False
            return True # done speaking

    def save_all_audio(self):
        flat_audio = np.hstack(self.audio)
        scipy.io.wavfile.write('{}/ALL.wav'.format(self.DATA_DIR), self.RATE, flat_audio)
        f, t, Sxx = signal.spectrogram(flat_audio, fs=self.RATE, window='hanning', scaling='spectrum')
        fig = plt.pcolormesh(t, f, 10 * np.log10(1 + Sxx), cmap='gray')
        fig.axes.get_xaxis().set_visible(False)
        fig.axes.get_yaxis().set_visible(False)
        plt.savefig('{}/spec_all.png'.format(self.DATA_DIR), bbox_inches='tight', pad_inches = 0)

    def convert_fileblock(self):
        block_counter = 0
        for file in glob.glob('{}/*.txt'.format(self.DATA_DIR)):
            block = np.loadtxt(file, dtype=np.int16)
            t0 = time.time()
            scipy.io.wavfile.write('{}/audio{}.wav'.format(self.DATA_DIR, block_counter), self.RATE, block)
            f, t, Sxx = signal.spectrogram(block, fs=self.RATE, window='hanning', scaling='spectrum')
            plt.figure(figsize=(self.INPUT_BLOCK_TIME, self.RATE / 4000), dpi = 100)
            fig = plt.pcolormesh(t, f, 10 * np.log10(1 + Sxx), cmap='gray')
            fig.axes.get_xaxis().set_visible(False)
            fig.axes.get_yaxis().set_visible(False)
            plt.savefig('{}/spec{}.png'.format(self.DATA_DIR, block_counter), bbox_inches='tight', pad_inches = 0)
            plt.close()
            print('Time to process block{}: {}'.format(block_counter, time.time() - t0))
            block_counter += 1
11 голосов | спросил syb0rg 17 AMpMon, 17 Apr 2017 04:47:51 +030047Monday 2017, 04:47:51

2 ответа


10

Я должен признать, что я понятия не имею, что цель кода (никогда не работала с распознаванием речи), но я немного знаю Python, поэтому я могу дать некоторые рекомендации по самому коде.

Производительность

Вы выразили желание оптимизировать производительность вашего скрипта. Но знаете ли вы первое правило правила оптимизации ? Если нет, это:

  • Не надо!

Второе правило:

  • Не ... еще!

И третье правило:

  • Профиль перед оптимизацией.

Позвольте мне перефразировать это: без тестов и профилирования вы (и я), скорее всего, оптимизируем неправильную часть кода и введем ошибку при этом.

Мне лично нравится pytest и line_profiler но неважно, какие инструменты вы выберете. Просто убедитесь, что вы знаете, где узкие места и что программа по-прежнему работает правильно после оптимизации узких мест. Прежде чем это сделать, я пока не буду беспокоиться о производительности.

Вместо этого я сосредоточусь на структуре кода.

Импорт

struct не используется, насколько я могу это понять, поэтому зачем его импортировать?

Также вы импортируете несколько пакетов, поэтому было бы неплохо их сортировать. Я обычно сортирую их по алфавиту и отдельные встроенные модули из сторонних модулей, например:

# Builtin modules
import glob
import math
import os
import time

# 3rd party modules
import matplotlib.pyplot as plt
import numpy as np
import pyaudio
import scipy.io.wavfile
from scipy import signal

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

try и except в функциях

Пожалуйста, посмотрите на свою функцию get_rms: это, по сути, однострочный код numpy, который защищен try и except.

Во-первых, здорово, что вы только ломаете Exception вместо того, чтобы иметь голый except! Но если вы когда-нибудь получите исключение, вы попадете в NameError в return rms, просто потому, что try не работает, а rms не определена.

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

def get_rms(block):
    return np.sqrt(np.mean(np.square(block)))

Я также удалил np.abs, потому что np.square возвращает само абсолютное значение, поэтому np.abs будет no-op.

Немного мелочей: если вы работаете с скалярами , вы также можете использовать math -функции. np.mean возвращает скаляр, поэтому вы можете использовать math.sqrt вместо np.sqrt. Я упоминаю это просто как мелочи, потому что маловероятно, что накладные расходы np.sqrt значительно повлияют на время выполнения вашей программы! Но если вам когда-нибудь понадобится микро-оптимизация, помните, что numpy поразительно быстро для массивов, но math быстрее скаляров.

Атрибуты класса и атрибуты экземпляра

У вас много констант в __init__, и важно инициализировать атрибуты изменчивые в __init__ (вместо атрибутов класса), но для неизменяемых атрибутов обычно проще инициализировать их в классе. Это вопрос предпочтения, поэтому это всего лишь предложение:

class AudioHandler(object):

    DATA_DIR = 'raw_data'
    RATE = 16000
    INPUT_BLOCK_TIME = 0.03  # 30 ms
    CHANNELS = 1
    INPUT_FRAMES_PER_BLOCK = int(RATE * INPUT_BLOCK_TIME)
    SENTENCE_DELAY = 1.1  # seconds
    MAX_SILENT_BLOCKS = math.ceil(SENTENCE_DELAY / INPUT_BLOCK_TIME)
    THRESHOLD = 40  # dB

    def __init__(self):
        self.pa = pyaudio.PyAudio()
        self.stream = self.open_mic_stream()
        self.save_counter = '0'.zfill(8)
        self.silent_blocks = 0
        self.listening = False
        self.audio = []

Еще одно замечание. Если вы используете подход контекстного менеджера, вы можете поместить строку self.stream = self.open_mic_stream() в __enter__ вместо __init__!

str.zfill илиstr.format

Я говорю об атрибуте save_counter. Вы инициализируете его как str, но вы регулярно используете его как int. С помощью str.format вы можете легко изменить строковое представление целого числа, поэтому я бы сохранил save_counter как int, а затем отформатируйте его, когда вам нужна строка. Например:

>>> '{:0>8}'.format(10001)
'00010001'

Это требует изменений во всем коде. Например, в save_block вам нужно будет изменить последние две строки на:

np.savetxt('{}/block{:0>8}.txt'.format(self.DATA_DIR, self.save_counter), flat_block)
self.save_counter += 1

Но я чувствую, что это более читаемо (и, вероятно, также немного быстрее).

return в циклах

В find_input_device вы можете легко избавиться от device, потому что либо вы return внутри цикла, либо цикл завершается без какого-либо совпадения:

def find_input_device(self):
    for i in range(self.pa.get_device_count()):
        devinfo = self.pa.get_device_info_by_index(i)

        for keyword in ['mic','input']:
            if keyword in devinfo['name'].lower():
                print('Found an input: Device {} - {}'.format(i, devinfo['name']))
                return i

    print('No preferred input found; using default input device.')
    # return None is implicit!

enumerate

В вашем методе convert_fileblock я увидел это:

block_counter = 0
for file in glob.glob('{}/*.txt'.format(self.DATA_DIR)):
    # ...
    block_counter += 1

Для этой цели: встроенный итератор enumerate:

for block_counter, file in enumerate(glob.glob('{}/*.txt'.format(self.DATA_DIR))):
    # ...

Одна небольшая проблема здесь: file не является хорошим именем переменной. Это (1) имя встроенной функции python2 и (2) filename будет более описательным в любом случае.

print vs. logging

В большинстве случаев вы никогда не захотите print. В зависимости от ваших предпочтений вы должны либо log, warn или raise. Например, в find_input_device сообщение устройства не всегда важно (за исключением случаев отладки или когда требуется подробный вывод). Вместо этого вы можете использовать logging :

logging.debug('Found an input: Device {} - {}'.format(i, devinfo['name']))

По умолчанию это сообщение скрыто - кроме случаев, когда вы делаете что-то вроде:

import logging
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)

, прежде чем запускать скрипт.

PEP8 и вызовы функций

Это:

    stream = self.pa.open(  format = pyaudio.paInt16,
                            channels = self.CHANNELS,
                            rate = self.RATE,
                            input = True,
                            input_device_index = device_index,
                            frames_per_buffer = self.INPUT_FRAMES_PER_BLOCK)

нарушает два правила PEP8:

  • Без пробелов после (
  • Нет пробелов вокруг = для аргументов ключевого слова

, поэтому это выглядит лучше:

    stream = self.pa.open(format=pyaudio.paInt16,
                          channels=self.CHANNELS,
                          rate=self.RATE,
                          input=True,
                          input_device_index=device_index,
                          frames_per_buffer=self.INPUT_FRAMES_PER_BLOCK)

, и если вы заботитесь о длине строки, вы также можете отступать несколько иначе:

    stream = self.pa.open(
            format=pyaudio.paInt16,
            channels=self.CHANNELS,
            rate=self.RATE,
            input=True,
            input_device_index=device_index,
            frames_per_buffer=self.INPUT_FRAMES_PER_BLOCK)

LBYL и EAFP

if not os.path.isdir(self.DATA_DIR) в save_block метод - красный флаг. Он может работать большую часть времени, но что, если каталог был создан вскоре после этого вызова и перед вызовом os.makedirs?

Как правило, вы должны просто попытаться создать его и поймать исключение, если оно уже существует. В более новых питонах makedirs поддерживает параметр exist_okay что упрощает это без использования try, except OSError

os.makedirs(self.DATA_DIR, exist_ok=True)

@Peilonrayz уже упомянул об этом, поэтому я не буду вдаваться в подробности LBYL и EAFP. Но, как правило, EAFP быстрее и надежнее (а часто и короче). Неудивительно, что это рекомендуемый стиль в python: -)

DRY

Методы save_all_audio и convert_fileblock share (на первый взгляд) некоторый общий код:

scipy.io.wavfile.write(...)
f, t, Sxx = signal.spectrogram(...)
fig = plt.pcolormesh(...)
fig.axes.get_xaxis().set_visible(False)
fig.axes.get_yaxis().set_visible(False)
plt.savefig(...)

Возможно, это можно было бы разделить на отдельный (частный) метод, который вызывается обоими методами.

Дополнительные контекстные менеджеры

Вы определили часть своего скрипта с помощью t0 = time.time(), print('Time to process block{}: {}'.format(block_counter, time.time() - t0)). Я бы предложил создать диспетчер контекста для этой задачи, потому что таким образом вы можете легко видеть, что происходит (потому что оно отступом), и вы можете повторно использовать его всякий раз, когда хотите что-то еще:

import time

class Timer():
    def __enter__(self):
        self._starttime = time.time()

    def __exit__(self, *args, **kwargs):
        time_elapsed = time.time() - self._starttime
        print(time_elapsed)  # or "logging.debug", see above

Пример:

with Timer():
    time.sleep(2)

Обратите внимание, что этот класс менеджера контекста добавляет некоторые служебные данные, но time.time в любом случае не подходит для «точного» анализа производительности. Если вы хотите точно выполнить некоторый код, вместо этого следует использовать модуль timeit.

Дополнительные комментарии!

ответил MSeifert 17 PMpMon, 17 Apr 2017 15:06:16 +030006Monday 2017, 15:06:16
7

PyAudio , похоже, не имеет with, однако вы должны добавить этот интерфейс к своему классу. PEP 343 объясняет, почему он был добавлен в Python, но в основном это происходит к синтаксическому сахару к try finally , Чтобы добавить это довольно просто, вы добавьте __enter__ и __exit__ и все. Преимущество этого заключается в том, что вам не нужно вручную закрывать поток, поскольку вы не в main.py. И поэтому я бы изменил ваш код, чтобы включить:

class AudioHandler(object):
    def __init__(self):
        ...
        self.pa = pyaudio.PyAudio()
        self.stream = None
        self.save_counter = '0'.zfill(8)
        ...

    def __enter__(self):
        self.open()
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self.close()
        return False

    def close(self):
        self.stream.close()

    def open(self):
        device_index = self.find_input_device()

        self.stream = self.pa.open(format=pyaudio.paInt16,
                                   channels=self.CHANNELS,
                                   rate=self.RATE,
                                   input=True,
                                   input_device_index=device_index,
                                   frames_per_buffer=self.INPUT_FRAMES_PER_BLOCK)

Я действительно не знаю библиотеки, которые вы используете, и поэтому я не могу их прокомментировать, но я бы рекомендовал использовать метод EAFP, а не LBYL для манипулирования файлами, чтобы вы не получили Ошибки TOCTOU .

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

И так все вышеизложенное изменило бы main.py грубо (я его не тестировал):

from record import AudioHandler
import shutil

if __name__ == '__main__':    
    invalid = 0
    with AudioHandler() as audio:
        shutil.rmtree(audio.DATA_DIR, ignore_errors=True)
        while not invalid:
            try:
                invalid = audio.listen()
            except KeyboardInterrupt:
                break

        audio.convert_fileblock()
        audio.save_all_audio()
ответил Peilonrayz 17 AMpMon, 17 Apr 2017 05:52:54 +030052Monday 2017, 05:52:54

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132