3. รู้จัก Speech Decoder

โดย SpeeChy

ในบทความที่ 3 นี้ ผมจะเจาะลึกเรื่อง Speech Decoder ซึ่งเป็นหัวใจสำคัญของ Speech recognition ก่อนจะเริ่มขอทบทวน ความจำนิดนึงถึงหน้าที่ของ Speech decoder นะครับ

เมื่อรับสัญญาณเสียงเข้ามา Decoder จะทำการสร้าง Word network ที่รวบรวม Word sequence ที่เป็นไปได้ทั้งหมดดังตัวอย่าง ในรูปที่ 1 แต่ละ Path ของ Network จะประกอบด้วย ค่าความน่าจะเป็นของการเกิด Word sequence (คำนวณโดย Language model) และความน่าจะเป็นของการเกิดสัญญาณเสียงนั้น เมื่อกำหนด Word sequence ใดๆ (คำนวณโดย Acoustic model) Decoder จะทำการ Search หา Path ที่ให้ Prob รวมสูงที่สุด Process นี้สามารถเขียนเป็นสมการ ได้ว่า

W = argmax P(W|O) = argmax P(O|W)P(W)(1)

โดยที่ W คือ Word sequence ที่ต้องการหา, O คือ Observation sequence ของ Input speech, P(O|W) เป็นความน่าจะ เป็นของการเกิด O เมื่อกำหนด W (Acoustic model), และ P(W) เป็นความน่าจะเป็นของการเกิด W (Language model)


รูปที่ 1 ตัวอย่าง Word network ในการ Search

Speech decoder หลักๆ ที่ใช้ในการ Recognize มี 3 วิธี

  1. One-pass Viterbi search
  2. One-pass Stack search
  3. Multi-pass search

One-pass หมายถึงมีกระบวนการ Search เพียงครั้งเดียว ก็ได้คำตอบเลย ส่วน Multi-pass ก็คือมีการผ่านกระบวนการ Search มากกว่า 1 ครั้งก่อนจะได้คำตอบ ในการ Search ครั้งแรกๆ จะ Output เป็น Hypothesis ที่มี Scope แคบลง เพื่อช่วยในการ Search ในครั้งถัดไป

ในบทความที่ 1 และ 2 ผมได้ใช้ Speech decoder ของ HTK ก็คือ Command ที่ชื่อว่า HVite ตัว HVite นี้เป็น Decoder แบบ One-pass Viterbi search เสียดายที่ว่า HVite ณ Version 3 นี้ยัง Support Language model ขนาด 2-gram เท่านั้น (3-gram ยังไม่ได้) ในบทความนี้ผมอยากจะใช้ 3-gram จึงขอนำเสนอ Decoder อีกตัวที่เป็น Open-source ที่สร้างโดย Kyoto University ชื่อว่า JULIUS ซึ่งเป็น Decoder แบบ Two-pass search ใจเย็นๆ นะครับหากว่าเริ่มงงๆ ค่อยๆ ติดตามไปครับ ผมจะค่อยๆ แนะนำการใช้งานจนกระทั่งอธิบายหลักการทำงานของการ Search ทุกวิธี

นอกจากนี้ ผมยังอยากจะนำเสนอ Open-source ยอดนิยมอีกตัวซึ่งใช้ในการเตรียม N-gram Language model (แต่เดิมในบทความ ที่ 1 และ 2 ผมเคยใช้ Command ใน HTK ในการสร้าง) ชื่อว่า CMU-SLM โดย Carnegie-Mellon University ในการทดลองของ ผมส่วนใหญ่ ผมจะใช้ HTK สร้าง Acoustic model, CMU-SLM สร้าง Language model, และ JULIUS เป็น Speech decoder นี่แหละครับ

คราวนี้มาถึงโจทย์ ในบทความนี้ผมจะขอใช้โจทย์เดิมที่ใช้ในบทความที่ 2 ซึ่งก็คือ Hotel Reservation Task (HRT) และผมจะขอนำ เอา Acoustic model ที่สร้างเสร็จแล้ว (ใครยังไม่เคยสร้างและอยากรู้วิธีสร้าง กลับไปอ่านที่บทความที่ 2 นะครับ) มาใช้เลยในบทความนี้ ส่วน Language model จะสร้างด้วย CMU-SLM โดยเริ่มต้นผมจะมี Text มาให้ มาเริ่มกันเลยนะครับ

ขั้นตอนที่ 1

  • ดาวน์โหลด HTK จาก http://htk.eng.cam.ac.uk มา install ตามขั้นตอน ที่สอนในเว็ป
  • ตั้ง Path ให้สามารถเรียกคำสั่ง Executable files ของ HTK หาก install ถูกต้อง เมื่อเรียกคำสั่งใดๆ ของ HTK โดยไม่ได้ใส่ Argument ใดๆ โปรแกรมจะแสดง Short help ของคำสั่งนั้นๆ
ขั้นตอนที่ 2
  • ดาวน์โหลด CMU-SLM จาก http://mi.eng.cam.ac.uk/~prc14/toolkit.html มา install ตามขั้นตอนที่สอนในเว็ป
  • ตั้ง Path ให้สามารถเรียกคำสั่ง Executable files ของ CMU-SLM หาก install ถูกต้อง เมื่อเรียกคำสั่งใดๆ ของ CMU-SLM เช่น text2wfreq -help จะแสดง Short help ของคำสั่ง text2wfreq
ขั้นตอนที่ 3
  • ดาวน์โหลด JULIUS จาก http://julius.sourceforge.jp/en มา install ตามขั้นตอนที่สอนในเว็ป
  • ตั้ง Path ให้สามารถเรียกคำสั่ง Executable files ของ JULIUS หาก install ถูกต้อง เมื่อเรียกคำสั่ง julius -help จะแสดง Short help ของคำสั่ง julius
ขั้นตอนที่ 4
  • ดาวน์โหลด Tutorial thaispeech3.tar.gz (795kb)
  • Unzip ด้วยคำสั่ง

    prompt> tar -zxf thaispeech3.tar.gz


    ในไดเรคทอรี thaispeech3/ จะประกอบด้วยไดเรคทอรีย่อยดังนี้

    config/เก็บไฟล์ที่กำหนดพารามิเตอร์ต่างๆ ในการเรียนรู้และรู้จำ
    script/เก็บ Script file ที่ใช้สั่งเพื่อเรียนรู้
    am/สำหรับเก็บไฟล์ Acoustic model ชื่อว่า NewMacros (อันเดียวกับที่สร้างใน Tutorial 2)
    lm/สำหรับเก็บไฟล์ Language model เริ่มต้นจะมี Text file ชื่อ hrt.text เป็น Text ที่จะใช้ในการ Train Language model
    mfc/สำหรับเก็บไฟล์ค่าสำคัญของเสียงที่ใช้ทดสอบ (Speech feature) เริ่มต้นจะยังว่างเปล่า
    wav/เก็บไฟล์เสียงที่เป็นตัวอย่างในการทดสอบ เป็นไฟล์ Windows PCM wav ธรรมดา

  • นอกจากนี้ ในไดเรคทอรี thaispeech3/ ยังมีงานได้แก่

    code.scpเก็บลิสต์ของไฟล์เสียง .wav คู่กับไฟล์ค่าสำคัญของเสียง .mfc ไฟล์นี้เป็นอินพุตของ code.sh ครับ เปิดดูได้ครับ เป็น ASCII text ธรรมดา
    code.shเป็น Script file เพื่อแปลงไฟล์เสียง .wav เป็นไฟล์ .mfc ซึ่งเก็บค่าสำคัญของเสียง
    test.scpเก็บลิสต์ไฟล์เสียงที่ใช้ในการทดสอบ แต่อยู่ในรูปแบบของไฟล์ .mfc แล้วนะครับ ลองเปิดดูครับ เป็น ASCII text ธรรมดา
    trainlm.shเป็น Script file สั่งให้ระบบสร้างโมเดลภาษาที่ใช้ในการรู้จำ

ขั้นตอนที่ 5
  • เริ่มต้น ต้องแปลงไฟล์เสียง .wav เป็นไฟล์ค่าสำคัญ .mfc ก่อน โดยใช้คำสั่งในไดเรคทอรี thaispeech3/

    thaispeech3> code.sh


    คำสั่งนี้จะทำการแปลง .wav เป็น .mfc ตามที่ลิสต์ไว้ใน code.scp ครับ ลองดูในไดเรคทอรี thaispeech3/mfc/ จะพบว่ามีไฟล์ .mfc เกิดขึ้นมา
ขั้นตอนที่ 6
  • ทำการสร้างโมเดลภาษาที่ใช้ในการเรียนรู้ โดยใช้คำสั่ง

    thaispeech3> trainlm.sh


    คำสั่งนี้ จะจัดการ Pre-process text file thaispeech3/lm/hrt.text และนำไป Train โมเดลภาษาซึ่งท้ายสุดก็คือ 2 ไฟล์ thaispeech3/lm/hrt_en.2gram และ thaispeech3/lm/hrt_en.rev3gram สังเกตนะครับ N-gram ที่ต้องเตรียม สำหรับ Tutorial นี้มี 2 ตัวคือ 2-gram และ Reverse 3-gram ซึ่งต้องใช้ใน JULIUS decoder ต่างกับกรณีของ HVite ซึ่งต้องการ N-gram เพียงตัวเดียว (รายละเอียดจะกล่าวต่อไป)
  • รายละเอียดขั้นตอนการ Train โมเดลภาษาสำหรับ JULIUS ดูใน การสร้าง LM ด้วย CMU-SLM toolkit ครับ
ขั้นตอนที่ 7
  • ในการรู้จำก็ง่ายๆ ครับ ในไดเรคทอรี thaispeech3/ ใช้คำสั่ง

    thaispeech3> julius -C config/jconfig


    Option -C สำหรับกำหนดไฟล์ Configuration ของ JULIUS คำสั่งนี้จะทำการโหลด Parameter ต่างๆ ในไฟล์ jconfig และจบที่ Prompt ให้ใส่ชื่อไฟล์ .mfc ที่ต้องการรู้จำ
  • ใส่ชื่อไฟล์ mfc ที่ได้เตรียมไว้ใน thaispeech3/mfc/

    enter MFCC filename> mfc/test1.mfc


    JULIUS จะทำการรู้จำและแสดงผลการรู้จำที่บรรทัดสุดท้าย
  • คราวนี้ ก็มาทำความเข้าใจการทำงานของ JULIUS รวมทั้งการ Set Parameter ในไฟล์ Configuration ใน Speech recognition ด้วย JULIUS


การสร้าง LM ด้วย CMU-SLM toolkit

วิธีการใช้งาน CMU-SLM toolkit อย่างละเอียดมีใน Documentation ของตัว Toolkit อยู่แล้ว ในที่นี้ผมจะมาชี้ให้เห็นเฉพาะขั้นตอนหลัก ที่ผมใช้ในการสร้าง N-gram ด้วย CMU-SLM หลักการสร้าง N-gram แสดงไว้ในรูปที่ 2


รูปที่ 2 หลักการสร้าง N-gram ด้วย CMU-SLM

Step 1 สร้างไฟล์ train.wfreq จาก train.text ด้วยคำสั่ง
   text2wfreq < train.text > train.text
โดยที่ train.wfreq จะแสดงคำที่มีทั้งหมดใน train.text พร้อมทั้ง frequency ของแต่ละคำ
 
Step 2 สร้างไฟล์ train.vocab จาก train.wfreq ด้วยคำสั่ง
   wfreq2vocab < train.wfreq > train.vocab
โดยที่ train.vocab เป็น List ของคำ (Vocabulary) ที่มีทั้งหมดใน train.text
 
Step 3 สร้างไฟล์ train.idngram โดยอาศัย train.vocab และ train.text
   text2idngram -vocab train.vocab -n 2 < train.text > train.idngram
ผลที่ได้คือ train.idngram เป็น Binary file ที่เก็บ index ของ N-gram ที่เกิดใน train.text ในที่นี้ -n จะเป็น ตัวกำหนดว่าจะสร้าง N-gram ที่ N เท่ากับเท่าใด (-n 2 คือ Bigram)
 
Step 4 สร้างไฟล์ train.2gram จาก train.idngram และ train.vocab
   idngram2lm -vocab train.vocab -n 2 -idngram train.idngram -arpa train.2gram
train.2gram เป็น N-gram สุดท้ายที่ต้องการครับ ใส่ -arpa หากต้องการ Output เป็น Text file และ -binary หากต้องการ Binary file (สามารถดู short help ของทุกคำสั่งได้ด้วยการใส่ -help)
 
ไฟล์ N-gram ที่ได้จะมี Format แบบมาตรฐาน ARPA ดังที่อธิบายไว้ใน Tutorial 2 ครับ คราวนี้ การสร้าง N-gram สำหรับ JULIUS decoder นั้นจะพิเศษกว่าปกติ นั่นคือ JULIUS ต้องการ N-gram สองตัว

  1. Bigram - ก็คือ Bigram ของ train.text ปกติ
  2. Reverse trigram - เป็น Trigram ของ train.text ที่ทำการ Reverse text แล้ว


รูปที่ 3 หลักการสร้าง N-gram สำหรับ JULIUS

รูปที่ 3 แสดงหลักการสร้าง N-gram สำหรับ JULIUS ซึ่งผมได้เขียน Script สำเร็จไว้ให้ใน trainlm.sh ดังนี้ครับ

Step 1 script/engtext config/hrt_th2en.table lm/hrt.text > lm/hrt_en.text
ทำการแทนคำภาษาไทยใน hrt.text ด้วยคำภาษาอังกฤษตามที่กำหนดใน hrt_th2en.table เพื่อตัดปัญหาการ Process text ภาษาไทยใน HTK รวมทั้งเติมสัญลักษณ์พิเศษ 'silB' และ 'silE' ที่ต้นและท้ายของทุกประโยค ซึ่ง แทน Silence หรือเสียงเงียบก่อนและท้ายเสียงพูดในทุกๆ ประโยค ผลที่ได้คือ hrt_en.text
 
Step 2 สร้าง hrt_en.2gram จาก hrt_en.text ตามขั้นตอนการสร้าง Bigram ด้วย CMU-SLM
 
Step 3 script/reverse.pl < lm/hrt_en.text > lm/hrt_en.revtext
ทำการ Reverse text ได้เป็น hrt_en.revtext
 
Step 4 สร้าง hrt_en.rev3gram จาก hrt_en.revtext ตามขั้นตอนการสร้าง Trigram ด้วย CMU-SLM
 


Speech recognition ด้วย JULIUS

JULIUS เป็น Speech decoder แบบ Two-pass คือทำการ Search หาผลการรู้จำสองรอบดังแสดงในรูปที่ 4


รูปที่ 4 หลักการทำงานของ JULIUS (Modified from JULIUS documentation)

1st pass

จะอาศัย Bigram และ Viterbi search ในการสร้าง Word trellis ซึ่งจะคล้ายกับ Word network ที่รวบรวม Word sequence ที่เป็นไปได้ แต่เพิ่มเติมตัวเลขที่บอกจุดเริ่มต้นและจุดสิ้นสุดของ Candidate word ในทุก Path ทั้งนี้ Path ของ Word sequence ที่มี Prob ตกต่ำกว่าค่า Threshold ที่กำหนด จะไม่อยู่ใน Word trellis เป็นการจำกัดขอบเขต ของการ Search ใน 2nd pass เพื่อลดเวลาในการ Search

2nd pass

จะอาศัย Reverse trigram คำนวณ Probability ของ Word sequence ใน Trellis ด้วย Stack search และทำการ Output word sequence ที่ให้ Prob สูงที่สุด ทำนองเดียวกันในขณะที่คำนวณค่า Prob ด้วย Reverse trigram, Path ที่มี Prob รวมต่ำกว่า Threshold ที่กำหนด จะถูกลบออกจากการเป็น Candidate

ง่ายมากๆ ครับ การใช้งาน JULIUS มีแค่ใช้คำสั่ง

julius -C config/jconfig

โดยที่ Option ต่างๆ ของมันจะกำหนดไว้ในไฟล์ jconfig ดังรูปที่ 5


รูปที่ 5 Configuration สำหรับ JULIUS

เราสามารถใส่ Parameter ที่ Command line ได้เช่นกัน เช่น

julius -C config/jconfig -dict config/hrt_en.dict

ซึ่งจะ Overwrite parameter ที่ Set ไว้ใน jconfig หากต้องการ Recognize แบบ Batch (Recognize ทีละหลายๆ ไฟล์) ก็สามารถ ทำได้โดย

julius -C config/jconfig < test.script > test.output

โดยที่ test.script เก็บ List ของ Input file ที่จะทำการ Recognize และ test.output เป็นผลการ Recognize

ผลการ Recognize ของ JULIUS จะมี 2 อันดังแสดงในรูปที่ 6 อันแรก (pass1_best) เป็นผล 1-best หลังจาก 1st pass อันที่สอง (sentence1) เป็นผล 1-best หลังจาก 2nd pass ซึ่งก็คือคำตอบสุดท้ายที่เราต้องการ (หมายเหตุนิดนึงว่า ผลการรู้จำในรูปที่ 6 เป็น สัญลักษณ์ของคำที่ผมกำหนดไว้ใน config/hrt_th2en.table)


รูปที่ 6 ผลการ Recognize ด้วย JULIUS


Viterbi search

ดังที่กล่าวมาแล้ว Word network ของการ Recognize จะมีขนาดใหญ่มากๆ ถึง Infinity จึงเป็นไปไม่ได้ที่จะ Search หมดทุก Path เพื่อหาประโยคที่ให้ Prob สูงที่สุด วิธีหนึ่งที่ใช้คือ Viterbi search

ผมขอยกตัวอย่าง Word network ง่ายๆ ดังรูปที่ 7 ณ t ใดๆ Viterbi search จะคำนวณค่า Prob รวมของทุก Path และจะเลือก Search ต่อเฉพาะ Path ที่มี Prob รวมสูงที่สุดดังแสดงในรูปเดียวกันครับ


รูปที่ 7 หลักการของ Viterbi search

เพื่อเพิ่มประสิทธิภาพของการ Search มีการพัฒนา Viterbi search 2 แบบ

  1. Beam search - ณ t ใดๆ จะทำการ Search ต่อเฉพาะ Path ที่ Prob > Threshold
  2. N-best search - ณ t ใดๆ จะทำการ Search ต่อเฉพาะ Path ที่มี Prob สูงสุด N path แรก

ข้อดีของ Viterbi search คือ Real time ได้ครับ กล่าวคือ Word network ที่ Viterbi search ไปนั้น สามารถสร้างขึ้นเรื่อยๆ ณ เวลาเดียวกับที่รับสัญญาณเสียงต่อเนื่องเข้ามา การ Search ก็สามารถกระทำได้ไปพร้อมๆ กันทีละ t (ทีละ Observation frame) บาง ครั้งจึงเรียกการ Search แบบนี้ว่า "Frame-synchronous Viterbi beam search"

ข้อเสียก็มีครับ มันจะทิ้ง Path ที่ Prob ต่ำเกินกว่าที่กำหนดไป ไม่กลับมาดูอีกเลย ทั้งที่บางครั้ง Path นั้นอาจจะมีค่า Prob รวมสูงขึ้นกว่า Path อื่นได้เมื่อ Search ถึง Word ท้ายๆ


Stack search

ในขณะที่ Viterbi search ณ t ใดๆ จะพิจารณา ทุก Path และเลือก Path ที่ Prob สูงในการ Search ต่อ Stack search จะ พิจารณา Path ใด Path หนึ่งทั้ง Path ก่อนจะไปพิจารณา Path อื่นๆ

ณ t ใดๆ Stack search จะเก็บ "Partial path" (ส่วนของ Path ที่ผ่านการ Search แล้ว) ใน Stack เรียงตาม Prob ของ Partial path นั้นๆ และจะดึงเอา Partial path ที่มี Prob สูงที่สุดมาทำการ Expand (เราเรียก Partial path ที่ถูกนำมา Expand ว่า Hypothesis) ในการ Expand จะทำการประกอบ Candidate word ลงไปที่ Path นั้นพร้อมทั้ง Estimate ค่า Prob ของใหม่ด้วยสมการ

fh(t) = ah(t) + bh(t)(2)

โดยที่ ah(t) เป็น Prob ของ Partial path ที่ทำการ Expand, bh(t) เป็น Estimation prob ของ Partial path นี้ที่ประกอบ Candidate word ลงไปแล้ว, fh(t) เป็น Prob ใหม่ที่ได้และ Hypothesis ที่ Expand แล้ว จะถูกนำมาเรียงลำดับใหม่ตาม Prob ใหม่เก็บใน Stack เช่นเดิม Process นี้จะ Iterate จนกระทั่งจบประโยค (สิ้นสุดสัญญาณเสียง)

ดูไปเหมือนจะคล้ายๆ กับ Viterbi search นะครับ แต่ข้อแตกต่างมันอยู่ที่การคำนวณค่า Prob ครับ ah(t) ก็ไม่ยากครับ มันคือ Prob รวม (Acoustic & Language modeling) ของ Partial path นั้นๆ แต่ bh(t) นี่สิที่ยาก เพราะว่าในทางทฤษฎี bh(t) ควรจะเป็น Prob ของ Path ส่วนที่เหลือจาก Partial path ไปจนถึงจบประโยค แต่เราไม่มีทางรู้ว่าประโยคจบลงที่ไหน ประโยคยาวกี่คำตั้งแต่ต้นครับ ดังนั้นในทางปฏิบัติ จึงมีวิธีการต่างๆ นานา ในการ Estimate bh(t) เมื่อประกอบ Candidate word ใดๆ ลงไปที่ Partial path อาทิเช่น วิธี "Acoustic model lookahead" ผมไม่ขออธิบายรายละเอียดของวิธีต่างๆ ในการ Estimate bh(t) ณ ที่นี้ครับ เพียงต้องการให้เห็น Idea ของการใช้ Stack search และสามารถไปอ่านเพิ่มเติมหากต้องการศึกษาลึกซึ้งต่อไป

ข้อดีของ Stack search คือ Path ที่ดีที่สุด จะไม่หลุดไปจากการ Search ตลอดการ Search (ไม่เหมือนกับ Viterbi ที่จะทิ้ง Path ที่ Prob ต่ำไปในขณะที่ Search) แต่ข้อเสียของ Stack search ก็คือ วิธีการ Estimate bh(t) จะต้องดีพอนั่นเองครับ


Copyright (c) 2004 Chai Wutiwiwatchai, Thai Speech Group