Std = np.std(VaryArray)
return Std
Следующий шаг – классификация интервалов по мощности. Его реализует функция getFeat. В ее основе лежит стандартная процедура kmeans.
from scipy.cluster.vq import kmeans,vq
def getFeat(Std):
Cent,_ = kmeans(Std,2)
Cent = sorted(Cent)
Out = vq(Std,Cent)
Features = Out[0]
return Features
В этой процедуре функция kmeans порождает два центроида, центры скопления значений стандартных отклонений интервалов. Процедура сортировки ставит не первое место меньшее из значений центроидов. Функция vq присваивает метку 0 или 1 каждому интервалу, при этом метка 0 означает , что данный интервал близок к меньшему из центроидов. Это означает, что такой интервал мы считаем шумовым.
Окрасим интервалы в разные цвета в зависимости от метки. Вот так выглядит размеченная часть речевого файла, состоящая из 17 фрагментов.
from matplotlib import pyplot as plt
Std = createStdDistr(In,SizeFragm)
Features = getFeat(Std)
NumFragm = 17
Beg = 10 * SizeFragm
for I in range(10,10 + NumFragm):
End = Beg + SizeFragm
if Features[I] == 0:
Col = 'k'
else:
Col ='r'
Arg = np.arange(Beg,End)
plt.plot(Arg,In[Beg:End],Col)
Beg += SizeFragm
Следует заметить, что конечный результат зависит способа вычисления характеристики интервала. Например, заменив стандартное отклонение на дисперсию, мы получим другое разбиение интервалов на классы. Использование максимального значения в качестве характеристики приводит к чувствительности решения к случайным выбросам. Достоинством процедуры kmeans является то, что не делается предположений о распределении стандартных отклонений.
Как было отмечено выше, результат классификации интервалов зависит от выбора характеристики интервала, однако, и сама классификация с помощью kmeans не является единственной возможной. Рассмотрим еще одну процедуру классификации пригодную для бинарного случая и используемую для построения черно-белых изображений. Здесь также не делается предположений о распределении исходных данных. Это алгоритм Отсу, а в его основе лежит гистограмма найденных характеристик интервалов (стандартное отклонение в нашем случае).
def otsu(Bins,Interv):
def oneStep(T):
'''
One step of the Otsu algo
0<T<NumBins
'''
Bins1 = Bins[:T]
Bins2 = Bins[T:]
Prob1 = Bins1.sum()
Prob2 = Bins2.sum()
Aver1 = sum(Bins1 * Middles[:T])/Prob1
Aver2 = sum(Bins2 * Middles[T:])/Prob2
return Prob1 * (Aver1 -Aver)**2 \
+ Prob2 *(Aver2 -Aver) **2
Bins = np.float_(Bins)
NumBins = len(Bins)
Middles = np.zeros(NumBins)
for I in range(len(Middles)):
Middles[I] = Interv[I] + Interv[I+1]
Middles *= 0.5
BinsSum = Bins.sum()
Bins /= BinsSum # Probabilities
Aver = sum(Bins * Middles)
Results = np.zeros(NumBins – 1)
for I in range(1,NumBins):
Results[I -1] = oneStep(I)
MxRes = np.amax(Results)
Pos = np.where(Results == MxRes)
return Middles[Pos[0][0] + 1]
Посмотрим на результат обработки того-же файла с помощью алгоритма Отсу
Bins,Interv = np.histogram(Std)
Level = otsu(Bins,Interv)
Result = np.where(Std<Level,0,1)
Мы разбили все интервалы на два класса, и теперь можем раскрасить тот же файл согласно новом разбиению. Полученный график имеет вид схожий с рисунком, полученным на основе kmeans.
Имея классификацию интервалов, можно попытаться выделить отдельные слова в файле. В основе процедуры "разделение" лежит следующая гипотеза. Слова разделяются последовательностью ШИ. Если ШИ оказался внутри слова, то это интервал между слогами. ШИ предшествующие слову и завершающие его включаются в слово. Заменяя каждый интервал нулем и единицей в зависимости от отнесения его к шуму или информации, получим ступенчатую последовательность Step. Разбиение на слова производится на основе этой последовательности.
Первая проблема, которую нужно решить – найти длины интервалов из нулей (или единиц) в этой ступенчатой последовательности. Таким образом вычисляется истинная длина интервала между отдельными информационным отрезками файла. Фактически мы пытаемся выделить границы слова, поэтому ищем интервалы, состоящие из 1. Такие интервалы назовем сегментами. Выделение сегментов из ступенчатой функции осуществляется с помощью функций 𝑠𝑒𝑙𝑒𝑐𝑡𝑆𝑒𝑔𝑚 и 𝑝𝑎𝑖𝑟𝑠11.
Первая функция превращает входную последовательность в строку, а вторая находит все сегменты в этой строке. Функция возвращает множество пар, определяющих начало и конец сегментов. В результате классификации каждый интервал заменяется нулем или единицей. Эту последовательность превращаем в одну строку.
def selectSegm(Step):
Marks2Str = [str(X) for X in Step]
Str = ''.join(Marks2Str)
Segms = pairs11(Str)
return Segms
Теперь в этой строке надо найти начало и конец каждого интервала, состоящего из 1.
def pairs11(A):
Out = []
if A[-1] == '1':
A = A + '0'
if A[0] == '1':
End =A.find('10')
Out.append(((0,End+1)))
Beg = End + 1
else:
Beg = 0
while True:
Beg = A.find('01',Beg)
if Beg == -1:
break
else:
End = Beg+1
End = A.find('10',End)
if End == -1:
break
else:
Out.append([Beg+1,End+1])
Beg = End +1
return Out
Вот пример, поясняющий работу этой функции. Исходная последовательность Step = np.int_([1,0,0,1,1,1,0,1,1]) превращается в строку A=’100111011’, а затем находим границы интервалов из 1.
Pairs = selectSegm(А)
print(Pairs)
[(0, 1), [3, 6], [7, 9]]
Следующий шаг является эмпирическим. Дело в том, что среди информационных интервалов, принадлежащих одному слову, может случайно попасть один или несколько интервалов из класса 0. Это приведет к разрыву слова на несколько частей. Чтобы избежать этого явления, вводим эмпирический параметр 𝐿𝑖𝑚, определяющий максимальный возможный разрыв между парами информационных интервалов одного слова. На данном этапе нам известны начала и концы сегментов из единиц. Если конец сегмента отстоит менее чем на 𝐿𝑖𝑚 интервалов от начала следующего сегмента, то оба сегмента объединяются путем замены интервалов между сегментами единицами. Наконец, заменив каждый интервал соответствующим отрезком исходного потока, получаем разбиение этого потока на слова. При частоте стробирования Fr=44100 и длине интервалов Fr/1000 выбор 𝐿𝑖𝑚 осуществляется из промежутка [25,45]. В результате проделанных манипуляций получаем функцию wordBorders(In, SizeFragm) , возвращающую список пар из начал и концов «слов». Слово взято в кавычки, поскольку таким образом могут быть выделены как фрагменты слова, произнесенного по слогам, так и целые предложения. Как будет показано ниже, это не имеет принципиального значения.