sty
03

Ukryte modele Markova

1. WSTĘP

Systemy automatycznego rozpoznawania mowy (ARM) stają się z roku na rok coraz bardziej popularne. Dzieję się tak za sprawą licznych potencjalnych zastosowań dla tego typu systemów: od prostych sterowników włączających i wyłączających światło na komendę [1][2], poprzez zaawansowane programy notujące, czy też wspomagające urządzenia dla osób niepełnosprawnych [3]. Ponadto mowa jest naturalnym sposobem komunikacji dla człowieka, stąd wielka atrakcyjność tego typu systemów dla potencjalnych użytkowników.

Dostępność gotowych narzędzi wspomagających pisanie programów do automatycznego rozpoznawania mowy, takich jak HTK czy Sphinx [6][7] sprawia, że tworzenie tego typu systemów jest znacznie prostsze niż jeszcze kilka lat temu. Ponadto dzięki temu autorzy nie muszą dokładnie zgłębiać wszystkich, częstokroć niezwykle skomplikowanych modeli matematycznych stojących u podstaw ARM. Niestety takie podejście ma również wady, gdyż nieznajomość modeli powoduje, że trudno jest w sposób świadomy dobierać parametry, czy choćby wybierać te z nich, które mogą mieć istotny wpływ na działanie systemu.

W artykule przedstawiono w sposób ogólny zastosowanie ukrytych modeli Markowa (hidden Markov models, HMM), jako narzędzia do budowy algorytmów rozpoznawania mowy.

W pracy zawarto praktyczne informacje  adresowane nie tylko do twórców programów do automatycznego rozpoznawania mowy, ale także dotyczące innych systemów bazujących na ukrytych modelach Markowa.

2. UKRYTE MODELE MARKOWA

Ukryte modele Markowa to zaawansowane modele statystyczne. Rozważany system może w dyskretnej chwili czasowej t przyjmować jeden z N stanów, zależnie od stanu w chwili poprzedniej (jest on łańcuchem Markowa rzędu pierwszego, tzn. jego stan zależy tylko od jednego stanu poprzedzającego) [4]. Wówczas można posłużyć się rysunkiem 1 do zobrazowania przejść pomiędzy stanami.

Rys.1. Łańcuch Markowa dla systemu z 3 dopuszczalnymi stanami (N=3)

Rys.1. Łańcuch Markowa dla systemu z 3 dopuszczalnymi stanami (N=3)

Przejścia opisuje się za pomocą kwadratowej macierzy prawdopodobieństw A=[aij]. Element aij jest to prawdopodobieństwo przejścia systemu ze stanu i do stanu j w kolejnej chwili czasowej [5]:

Do opisu systemu nie wystarczy sama macierz A. Konieczna jest również znajomość macierzy Π, która zawiera prawdopodobieństwa wystąpienia i-tego stanu na początku sekwencji stanów.

Znając obie macierze: A oraz Π, możliwe jest obliczenie prawdopodobieństwa wygenerowania przez system sekwencji stanów q=(q0, q1, …, qT):

W przypadku rozpoznawania mowy rozważa się jednak nieco odmienną sytuację, gdy łańcuch q uznawany jest za niejawny (ukryty). Znana jest natomiast sekwencja obserwacji O. W przypadku mowy, obserwacja jest to wektor cech wyekstrahowanych z pojedynczej ramki nagrania.

Rys.2. Sekwencja obserwacji (wektorów cech wyekstrahowanych z nagrania mowy)

Rys.2. Sekwencja obserwacji (wektorów cech wyekstrahowanych z nagrania mowy)

Dla każdego ze stanów systemu istnieje pewne prawdopodobieństwo b, że w trakcie przebywania systemu w tym stanie wygenerowana zostanie obserwacja Ot:

Ponieważ nie da się przewidzieć liczby różnych obserwacji, które mogą wystąpić (może być ich nieskończenie wiele), do opisu prawdopodobieństwa pojawienia się obserwacji Ot w czasie, gdy system znajduje się w i-tym stanie należy posłużyć się ciągłą funkcją gęstości prawdopodobieństwa. Najczęściej używa się kombinacji liniowej M rozkładów Gaussa:

Gdzie cim oznacza współczynnik wagowy, a N(µ,Σ) jest to rozkład normalny charakteryzowany przez wektor wartości średnich µ oraz macierz kowariancji Σ.

Ukryte modele Markowa oznacza się często jako λ [5]:

Posiadając wszystkie te informacje, możliwe jest obliczenie prawdopodobieństwa wygenerowania sekwencji obserwacji O przez system λ:

LITERATURA

1.      Haleem M.S.: Voice controlled automation system. Multitopic Conference IEEE International, Karachi 2008, s. 508-512

2.      Kubik T., Sugisaka M.: Use of a cellular phone in mobile robot voice control. SICE Proceedings of the 40th SICE Annual Conference, Nagoya 2001, s. 106-111

3.      Simpson R.C., Levine S.P.: Voice Control of a Powered Wheelchair. Neural systems and rehabilitation engineering, 10, 2, 2002, s. 122-125

4.      Elliott R.J., Aggoun L., Moore J.B.: Hidden Markov Models: estimation and control. Springer, New York, 1995, s. 3-19, ISBN 03-87943-64-1

5.      Juang B.H., Rabiner L.R.: Hidden Markov Models for Speech Recognition. Technometrics, 33, 3, 1991, s. 251-272

6.      Ma G., Zhou W., Zheng J., You X., Ye W.: A Comparison between HTK and SPHINX on Chinese Mandarin. Artificial Intelligence, China 2009, s. 394-397

7.      Young S.J, Woodland P.C., Byrne W.J.: Spontaneous speech recognition for the credit card corpus using the HTK toolkit. Speech and Audio Processing, IEEE Transactions on, 2, 4, 1994, s. 615-621

8.      Openshaw J.P., Sun Z.P., Mason J.S.: A comparison of composite features under degraded speech in speaker recognition. Acoustic, Speech and Signal Processing, 2, 2, Minneapolis 1993, s. 371-374

9.      Tolba H., O’Shaughnessy D.: Automatic speech recognition based on cepstral coefficients and a mel-based discrete energy operator. Acoustic, Speech and Signal Processing, 2, 2, Seattle 1998, s. 973-976

sty
02

Rozpoznawanie mówcy

Identyfikacja a weryfikacja

Systemy identyfikacji na podstawie głosu mają za zadanie stwierdzić kim jest osoba, której głos został podany na wejście systemu. Odbywa się to bez jakiejkolwiek sugestii. System korzysta jedynie ze swojej bazy wzorców aby stwierdzić czy głos pasuje do którejś z osób upoważnionych. Może również stwierdzić, że osoba nie widnieje w systemie. Z kolei systemy weryfikacji mają za zadanie stwierdzić czy osoba, której głos został podany systemowi jest tą, za którą się podaje. System może tutaj udzielić jednej z dwóch odpowiedzi: tak lub nie.

Systemy zależne od tekstu

Są to systemy, w których oprócz tego kto mówi istotne jest również co mówi. Takie programy są w stanie rozpoznać użytkownika tylko jeśli wypowie odpowiednią frazę. Może to być pojedyncze słowo lub zdanie. Istnieją również systemy niezależne od tekstu, które rozpoznają użytkowników bez względu na to co zostanie przez nich wypowiedziane.

Ogólny schemat

Każdy system składa się z 3 części. Po pierwsze jest to część odpowiedzialna za akwizycję sygnału oraz wydobycie z niego wektora interesujących nas cech. Druga część to odwołanie się do związanego z systemem banku mówców (tzw. słownika) w celu obliczenia podobieństwa w przyjęty wcześniej sposób. Trzeci etap stanowi proces decyzyjny. System mając na uwadze zadane kryteria stwierdza tożsamość danej osoby lub informuje, że identyfikacja się nie powiodła.

Metody

Jest kilka standardowych metod wykorzystywanych do rozpoznawania mówcy. Można tutaj wykorzystać:

  • Współczynniki mel cepstrum
  • Współczynniki LPC
  • Ton krtaniowy (podstawowy)
  • Ukryte modele Markowa

Można również użyć metody mieszanej, to znaczy stworzyć wektor składający się z kilku cech.

Osobną kwestią jest dobór klasyfikatora, czyli mechanizmu, który będzie decydował czy podany wektor cech pasuje do którejś z klas (odpowiada jakiemuś użytkownikowi). Istnieje tutaj duża różnorodność metod, od najprostszych typu metoda najbliższego sąsiada do bardziej skomplikowanych jak sieci neuronowe.

paź
07

BlueCove mutlithread with multidevice

W przypadku BlueCove problemem może być zaimplementowanie kilku wątków działających na jednym urządzeniu (donglu), sprawa jeszcze bardziej się komplikuje gdy mamy kilka urządzeń. Niewłaściwa obsługa stosu w przypadku biblioteki BlueCove może powodować pojawienie się kilku błędów:

  • IllegalArgumentException BlueCove Stack already initialized
  • BluetoothStateException No BluetoothStack or Adapter for current thread

Poniżej prezentuję kod, który umożliwia takie działanie naszego programu:

//fragment klasy głównej:

BlueCoveImpl.useThreadLocalBluetoothStack();
try {
//sprawdzamy dostępne dongle
Vector devices = BlueCoveImpl.getLocalDevicesID();
//załóżmy, że mamy 2 dongle
if(devices.size() == 2){
Dongle d1 = new Dongle((String) devices.get(0));
Dongle d2 = new Dongle((String) devices.get(1));
d1.start();
d2.start();
}
}catch (BluetoothStateException ex) {
System.exit(1);
}

W dalszej części przedstawiony zostanie kod dwóch klas, Dongle i SecondThread. Pierwsza z nich posiada wątek działający na wskazanym urządzeniu oraz dodatkowo uruchamia drugi wątek (klasa SecondThread). Efektem uruchomienia powyższego kodu będą zatem 4 wątki, po 2 na każdym donglu.

public class Dongle{
private String device;
private Object stack = null;

public Dongle(String device) {
this.device = device;
//pierwsze urządzenie musimy traktować inaczej
//aby nie otrzymać błędu, że urządzenie jest już używane,
//bądź stos został już zainicjowany
if(device.compareTo("0")==0)
try{
stack = BlueCoveImpl.getThreadBluetoothStackID();
} catch (BluetoothStateException ex) {}
}
private Thread dongleThread1= new Thread() {
public void run() {
if(device.compareTo("0")!=0)
try {
stack=null;
//ustawiamy urządzenie, na którym będzie działał wątek
//nie możemy tak zrobić z donglem 0!
BlueCoveImpl.setConfigProperty("bluecove.deviceID", device);
LocalDevice.getLocalDevice();
}
catch (Exception e) {
e.printStackTrace();
}finally {
try {
//obiekt id stosu - dzięki niemu będzie można podpiąć
//inne wątki do tego samego dongla
stack = BlueCoveImpl.getThreadBluetoothStackID();
//wiążemy wątek z urządzeniem
BlueCoveImpl.setThreadBluetoothStackID(stack);
} catch (Exception e) {
e.printStackTrace();
}
}
else{
//w przypadku dongla 0 wiążemy go tylko z wątkiem
BlueCoveImpl.setThreadBluetoothStackID(stack);
}

if(stack!=null) runSecondThread(stack,device);

while (true) {
//rób coś ..
//...
try {
Thread.sleep(1000);
} catch (InterruptedException ex) {}
Thread.yield();
}//while
}//run
}//thread
public void runSecondThread(Object st, String d){
SecondThread secTh = new SecondThread(st,d);
}
public void start() {
dongleThread1.start();
}
}

Kod klasy SecondThread:

public class SecondThread implements  Runnable{
private String dev;
Object stack = null;
Thread secTh = null;

public Server(Object s,String dev){
this.dev = dev;
stack = s;
secTh = new Thread(this);
secTh.start();
}
public void run(){
try {
BlueCoveImpl.setThreadBluetoothStackID(stack);
while(true){
//rób coś
//..
try {
Thread.sleep(1000);
} catch (InterruptedException ex) {}
Thread.yield();
}
} catch (Exception ex) {
}

}//class

paź
02

Operacje na plikach J2ME

Niestety, ale obsługa plików na telefonach nie jest łatwym zadaniem. Najprostszym narzędziem, które do tego służy jest klasa FileConnection (javax.microedition.io.file.FileConnection), jednak jest ona opcjonalna i nie występuje na wszystkich urządzeniach. Dlatego zanim wykorzystamy ją do operacji na plikach należy się upewnić, że istnieje:

String v = System.getProperty("microedition.io.file.FileConnection.version" );
if( v != null ){
//operacje na plikach
}else{
//nie można wykonywać operacji na plikach
}

Zapis plików:

//plik zapisany bajtowo
byte [] plik;
...
String nazwa = "plik.jpg";
FileConnection fc = (FileConnection) Connector.open(System.getProperty("fileconn.dir.graphics")+nazwa, Connector.READ_WRITE);
if (!fc.exists()) fc.create();
DataOutputStream is = fc.openDataOutputStream();
//zapis tablicy bajtów
is.write(plik);
is.close();
fc.close();

W powyższym przykładzie posiadamy obraz pod postacią tablicy bajtów (byte[] plik), który zapisujemy na telefonie w katalogu z grafiką. Należy zwrócić uwagę na to, że ścieżka do tworzonego pliku nie jest podawana a priori: System.getProperty(„fileconn.dir.graphics”)+nazwa.
W urządzeniach mobilnych zazwyczaj mamy ściśle określone katalogi, do których mamy dostęp z poziomu javy. Gdybyśmy próbowali tworzyć plik w innym miejscu otrzymalibyśmy błąd Access denied.

Inne możliwe ścieżki:
fileconn.dir.videos
fileconn.dir.graphics
fileconn.dir.tones
fileconn.dir.music
fileconn.dir.recordings
fileconn.dir.memorycard
fileconn.dir.private
fileconn.dir.themes

Po otwarciu strumienia DataOutputStream możemy również zapisywać proste typy danych, niekoniecznie same tablice bajtów:

DataOutputStream is = fc.openDataOutputStream();
is.writeUTF("Ciąg znaków");
is.writeInt(1234);
is.writeBoolean(true);
is.close();

Odczyt plików jest analogiczny:

FileConnection fc = (FileConnection) Connector.open(System.getProperty("fileconn.dir.graphics")+nazwa, Connector.READ_WRITE);
if (fc.exists()) {
byte[] b = new byte[(int)fc.fileSize()];
DataInputStream is = fc.openDataInputStream();
is.read(b);
is.close();
}
fc.close();

Podobnie jak poprzednio możemy odczytywać proste typy danych (o ile wcześniej zostały zapisane w takiej kolejności w pliku):

DataInputStream is = fc.openDataInputStream();
String s = is.readUTF();
int i = is.readInt();
boolean b = is.writeBoolean();
is.close();

wrz
27

ResponseCode 144 (0×90)

Wysyłając dane przy użyciu bluetootha można uzyskać dość nieoczekiwaną odpowiedź, która nie jest nawet zdefiniowany w klasie javax.obex.ResponseCodes.

Poniższy kod prezentuje w jaki sposób dane zostały wysłane i odebrane:
WYSYŁANIE DANYCH (serwer) natępuje w momencie gdy klient zgłosi się do serwera:

//obiekt obsługujący m.in. operacje PUT i GET, musimy go sami zdefiniować
RequestHandler rh = new RequestHandler(this);
SessionNotifier sn = (SessionNotifier) Connector.open(url);
//oczekiwanie na zgłoszenie się klienta:
Connection con = sn.acceptAndOpen(rh);
//fragment klasy obsługującej operację GET
class RequestHandler extends ServerRequestHandler{
//nasze dane do wysłania
String [] list = {"str1","str2","str3"};
...
public int onGet(Operation op){
DataOutputStream os = op.openDataOutputStream();
//zapis długości tablicy
os.writeInt(list.lenngth);
//zapis danych
for(String s : list) os.writeUTF(s);
os.close();//(*)
...
}
}

ODBIÓR DANYCH (klient)

ClienstSession ses = (ClientSession) Connector.open(serviceURL);
HeaderSet resp = ses.connect(null);
code = op.getResponseCode();
HeaderSet hdrs = ses.createHeaderSet();
Operation op = ses.get(hdrs);
DataInputStream dis = op.openDataInputStream();
//jako dane wysyłana jest tablica Stringów, najpierw przesyłana jest wielkość tablicy
//a następnie kolejne ciągi znaków
int dlugosc = dis.readInt();
String [] lista= new String[dlugosc];
for(int i=0; i < dlugosc;i++) lista[i] = dis.readUTF();
int code = op.getResponseCode();

Okazuje się, że code w powyższej linijce ma wartość 144 (0×90) zamiast spodziewanego OBEX_HTTP_OK (160). Oznacza on, że dostępne są kolejne pakiety (nie przeczytaliśmy wszystkiego). Jest to tym dziwniejsze, że jeśli wywołamy:

int ilosc_bytow = dis.available();

otrzymamy 0 (ilość nie przeczytanych bajtów).
Co więcej jeśli próbujemy dalej komunikować się z serwerem, ignorując tę dziwną odpowiedź, wyrzuci on błąd:
java.io.IOException: Client not requesting data . Związany jest on z tym, że serwer nigdy nie osiągnął linijki oznaczonej (*):  os.close().
Próba nawiązania kolejnego połączenia przez klienta:

dis.close();
op.close();
ses.disconnect(null);
resp = ses.connect(null);
code = resp.getResponseCode();

zakończy się niepowodzeniem. Zwrócony zostanie kod: 192 (OBEX_HTTP_BAD_REQUEST).

Jak naprawić sytuację, aby możliwa była dalsza komunikacja?
Otóż po wczytaniu wszystkich informacji (w tym przypadku wszystkich Stringów) należy ponownie spróbować wczytać coś ze strumienia:

//wczytujemy wszystkie stringi:
for(int i=0; i < dlugosc; i++) lista[i] = dis.readUTF();
try{
dis.readUTF();
}catch(EOFException e){}

Bardzo ważne jest umieszczenie tego kodu w bloku try ponieważ zawsze wyrzuci on wyjątek EOFException. Dzięki zastosowaniu powyższego tricku strumień na serwerze osiągnie kod oznaczony (*), a klient będzie mógł kontynuować komunikację.

Starsze posty &laquo