2016年8月30日 星期二

Microsoft Cognitive Services (3) - 在你的app中直接加入語音辨識

時代真的不一樣了。

我還記得剛使用iPhone的時候,那個著名的Siri,辨識我說話的效果不是很好,但因為我常常需要做語音筆記,並且轉到待辦事項或是行事曆紀錄裡面。因此,我下載了另一個叫做Dragon Dictation的app,哇,用起來驚為天人,辨識率出奇的好,讓我龍心大悅大開眼界。這,已經是幾年前的事情了。

直到最近,自己有需要在開發App時導入語音辨識,才開始又survey起相關的套件,萬萬沒想到,怎麼現在做語音辨識可以簡單成這樣?

語音辨識真的不是新玩意兒了,早幾年前微軟就已經推出過text/speech之間的轉換工具,但辨識率(特別是對中文的支援)依舊是大家是否會採用的關鍵,沒想到幾年不見,現在Cognitive Services中的Speech Recognition已經有非常好的成效,不僅僅辨識率高,也支援中文,還有提供C#、android、iOS、javascript…等等等的sdk,都開放成這樣了,如果再不用那就是對不起自己了。

現在要使用此服務,非常的簡單,參考本系列第一篇,先申請好key之後,在你的專案中引入nuget package,然後就可以直接用了:

接著,我們來看程式碼的寫法:

請先建立一個全域的micClient物件,該物件可以從SpeechRecognitionServiceFactory靜態類別的CreateMicrophoneClient方法產生,可用來直接access我們電腦的麥克風,並進行收音並且開始辨識。

記得建立該物件的instance時要傳入辨識方法(SpeechRecognitionMode.LongDictation)與語言地區(zh-TW)等參數,當然也少不了key。

接著,我們hook幾個事件,分別是:

  1. OnMicrophoneStatus : micClient狀態發生改變時
  2. OnPartialResponseReceived : 部分辨識完成時
  3. OnResponseReceived : 整段語句辨識完成時
  4. OnConversationError : 發生錯誤時

顯而易見的,你可以在OnResponseReceived 事件發生時,抓取辨識結果,因此我們聚焦在這個事件的Event handler(其他幾個事件你不hook也行)。

別忘了最後要呼叫StartMicAndRecognition方法。

剛才說到,主要還是OnResponseReceived 這個事件,你會發現,開發人員可以透過回傳的SpeechResponseEventArgs參數,來取得相關的結果:

上面的程式碼中先判斷了目前正在進行的語音辨識的狀態,如果一句話告一段落,或是Timeout,則我們透過非同步的方式呼叫了MicClient的EndMicAndRecognition結束辨識動作。

但重點其實最後一行,我們如何抓取辨識的結果?

抓取辨識結果的方式很簡單,你可以透過SpeechResponseEventArgs參數e中的Results屬性取得,該屬性指向一個陣列,該陣列會回傳幾組最佳的辨識結果(如果有的話),而每一組辨識結果的型別則是RecognizedPhrase,該物件有底下幾個屬性:

其中Confidence則辨識結果可信度,共有none, low, normal, high等四種可能,而DisplayText、LexicalForm、InverseTextNormalizationResult、MaskedInverseTextNormalizationResult都是辨識的結果,但用途不同,依般來說,如果顯示給用戶看可以直接採用DisplayText,而LexicalForm適合作為程式判斷用途。另外兩個是把辨識結果中的某些短字詞做了替換,詳細說明請參考底下這裡:
https://staging.www.projectoxford.ai/Files/Doc/Speech/Windows/html/cbd61214-dc0f-a674-273a-751145514068.htm

OK,透過這組服務,辨識一段話變得非常容易:


你可以在這邊看到微軟提供的相關範例完整程式碼。

2016年8月28日 星期日

關於bot framework (5) - 透過.net SDK使用LUIS

在你知道了LUIS的功能之後,接著我們就來看看如何透過程式碼叫用LUIS。

你知道LUIS是一個服務,且用的通訊方式是http與JSON,因此你當然也知道可以透典型的http get來抓取,並且把JSON轉成js或c#看得懂的類別來操作。

不過,如果你使用VS2015、寫C#或VB,以前沒有適合的nuget package可用,從上一周開始,你可以直接從nuget上使用底下這個套件,來呼叫LUIS的功能:

安裝此套件之後,使用起來非常簡單:

//建立LuisClient
Microsoft.Cognitive.LUIS.LuisClient lc =
    new Microsoft.Cognitive.LUIS.LuisClient(
        LUIS AppID, key);

this.textBoxResult.Text = "分析中...";
//Call Luis API 查詢
var ret = await lc.Predict(textBoxQuery.Text);
if (ret.Intents.Count() <= 0) return;

var msg = "";
foreach (var item in ret.Intents) //顯示intent
{
    msg += $"\n intent: {item.Name}  score:{item.Score} ";
}
msg += "\n\n";
foreach (var item in ret.Entities)  //Entities
{
    msg += $"\n Entities: {item.Value[0].Name} {item.Value[0].Value}   score:{ item.Value[0].Score} ";
}
//顯示於TextBox
this.textBoxResult.Text = msg;


這幾乎不用解釋了。

你可以使用LuisClient 建立一個instance,記得傳入LUIS AppID與key,透過Predict以非同步方式呼叫,把要查詢的句子(Query)傳入當參數,回傳值透過intents與Entities兩個集合屬性傳回,LUIS會回傳給你它所分析出的所有可能性,你可以透過score找到分數以判斷最高的可能性。

that’s it.
have fun.

2016年8月24日 星期三

關於bot framework (4) - 使用LUIS,讓你的bot理解用戶輸入文字的意義

LUIS全名是Language Understanding Intelligent Services,顧名思義,是一個提供語句理解能力的服務,請留意,LUIS並非去分析語句(這是Text Analytics的功能),而是去嘗試理解一個句子的意義,找出用戶的企圖(intents)與相關聯的entities。

而這個功能,對於我們的chatting bot,非常的重要。由於文字bot的輸入介面,用戶往往可以自由隨興的打字輸入,你根本無法限制用戶輸入什麼,而且人很有趣,只要沒有限制,就會竭盡所能地嘗試,你只要開放一個text chatting bot,就會有人手ㄐㄧㄢˋ的亂打一些有的沒的,然後bot沒理解出他的意思,他就笑這個bot很笨。(這樣很有成就感?)

因此,就對談機器人來說,首要目標之一就是理解用戶輸入的內容,而LUIS是實現這個功能的重點服務。

首先,我們需要知道一些概念,LUIS的基本功能(以後有空再說複雜的),就是區分出用戶的意圖和相關的entities。

拿點餐來說,如果我們要點一個早餐,用語大概會是:

  • 我要點一份燒餅油條
  • 麻煩你我需要一份蛋餅
  • 給我來個大亨堡
  • 三明治帶走

    上面這四句話,基本上intent都是點餐,而其中的entities則是餐點,具體的內容是燒餅油條、蛋餅、大亨堡、三明治。而LUIS的目的,是幫我們在雲端分析各種不同的句型,找出用戶的意圖,並且抓出entities。

因為,用戶不可能只用上面這四種說法點餐,有時候可能會換成:

今天請幫我來一個飯糰

這個句型就跟上面四種截然不同,但LUIS能夠從雲端大量的語句資料庫中,幫我們進行歸納,判斷(猜測)這個句型的意圖,一開始可能判斷不對,這時人工可以介入,指導LUIS,讓它理解上面這個句型就是點餐,這樣下次LUIS就知道了。而其中的entities也是,餐點種類繁多,第一次LUIS肯定不知道有個餐點叫做『飯糰』,所以它抓不出這個entity,但你可以指導它,它就知道下次看到飯糰,就可以將其視為entity。

有了這個基礎之後,我們就可以透程式碼,把從bot接收到的語句,丟給LUIS進行判斷,LUIS就會告訴我們,用戶所輸入的這個語句,其意圖intent與entities為何。

好,概念交代完畢,接著來實作,首先,請用Microsoft Account登入,登入後到底下網址註冊一個LUIS服務:
https://www.luis.ai

出現底下畫面並且跑一陣子,是正常的:

接著你會看到底下畫面,請點選 New Application:
建立LUIS App(其實我覺得叫Service比較好)時,你必須填妥底下1-4的資訊,App名稱可以用中文,而其中3的部分,關係到你的LUIS在分析語句的時候,所優先採用的資料庫,選擇正確的項目對效能會比較有幫助。

最後別忘了步驟4的Culture選中文:
建立該App會需要一點時間,完成後你可以在主畫面看到該App。可以點選Edit,進入編輯後台:

進入後台編輯畫面後,首先,我們來建立一個intent,整個情境是我們要為小吃店做一個客服bot,因此首要任務就是我們的bot必須要看懂客戶的點餐:
所以,我們先建立一個Intnets,需要輸入的資料如下。輸入完畢後按下Save:

在上圖中,我們建立一個intent,主要目的是讓bot看得懂客戶點餐的用語,因此下方的example我們給了一句"我要點一份燒餅油條",來教導bot理解這是點餐的意思。
請注意save之後,在主畫面你會看到左方多了一個 “客戶點餐” 的intents,畫面中間是我們剛輸入的語句,這時請點選下拉上圖中右方的下拉清單,選出"客戶點餐"這個intent,然後按下submit,這個動作,就是教導LUIS說,這句話是點餐的意思。

但這樣LUIS並不知道"燒餅油條"是一個entity啊,因此我們要在左方建立一個Entity:
本小吃店的餐點有兩種,一種是吃的,一種是喝的。因此我們建立了一個餐點Entity,並且設定了Children,分別是主餐、飲料。

完成後,我們可以在主畫面訓練這個LUIS,讓它不僅看懂這句對話,並且抓出entity,我需要特別把底下這個動作錄製起來,讀者會比較容易理解(而且建議你用Chrome/IE操作)。你會發現,當你要訓練LUIS抓出entity,可以在該語句上,把特定字詞用滑鼠框起來,框住的範圍會反白,這時會跳出你可以選擇的entities,你可以選擇後,再submit:

這樣LUIS就知道這個句子的intent與entity了。
我們可以用同樣的方式把前面提到的四種相關的訂餐語句都輸入進去,記得要選取intent與entity喔。

如果你有設定錯誤需要調整,或想要看目前LUIS學習了那些語句,可以點選review labels:

從上圖中可以看到LUIS目前可以理解的句子,當滑鼠停留在黃框上,會出現選定的entity。

接著,請點選畫面左下角的train,讓LUIS完整的跑一次訓練:

完成之後,接著我們就可以進行測試了。請點選畫面左上方的Publish,把這個LUIS發布成為一個Web Services App:

請在發布畫面按下圖中的按鈕:

成功後即可進行測試,你可以在上圖的query視窗中,試著輸入一句對話,例如: 『我要一份蘿蔔糕』,我們刻意輸入和先前訓練時稍微不同的對話:

回傳的結果是:

這表示,LUIS知道(成功猜測)這句話的意思是點餐,但尚且不知道entity是啥(當然,LUIS還不認得蘿蔔糕),沒關係,我們進入管理後台,你可以在Search/Suggest…等地方找到尚未被LUIS完整識別出來的語句,LUIS會去猜測這語句的意圖,但你可以透過這個管理畫面糾正或訓練它:
找到之後,我們可以用同樣的選取方法,明確的告訴LUIS,蘿蔔糕也是一個主餐,submit之後,別忘了重新訓練並且publish。然後,你再用同一句話去問LUIS,你會發現,回傳的結果是:


有沒有,蘿蔔糕被認為是entity了。如此這般,隨著用戶輸入的語句越來越多,我們也可以逐步修正並訓練LUIS判斷的結果,其結果就會越來越準確。如此一來,LUIS就真的會看得懂大部分用戶的訂餐語句,並且判斷出要點的餐點為何。

從上面的介紹中你也不難發現,呼叫LUIS的方法很簡單,就是一個單純的http get,回傳的結果就是JSON,我們下一篇再來看,如何透過SDK呼叫LUIS,讓我們的bot可以輕易的與LUIS整合。

2016年8月22日 星期一

關於bot framework (3) - 建立一個最基本的bot (v3新版)

前陣子,MS把bot framework稍微簡化了一下(其實是偷渡了Skype bot整合),主要的調整是bot connector這一段(OS:啊這段不就是核心的部分…這改了實際也不算小改),連同註冊頁面整個都做了調整。

現在,如果你要建立一個新的bot,依舊可以用自己的Microsoft Account,到底下網址:
https://dev.botframework.com/bots

點選畫面上的register a bot,可以註冊一個新的bot:

不過,請回憶一下先前我們在v1談過的這張圖:

嚴格說起來,我們所謂的註冊一個bot,意義上是跟微軟的bot framework註冊一個Bot Connector,我們將來可以透過這個bot connector來串接不同的bot client(channels),預設狀況下,最基本的bot client就是Web chat,也就是bot connector自帶的一個對談介面(用戶端)。

而左邊的Your bot,其實才是我們要撰寫的對談機器人的核心,也就是chatting對談邏輯(Business Logic)的主要實作部分,這個實作(如果用C#實作的話)基本上是一個WebApi網站,提供restful的API讓bot connector來呼叫。如此一來,我們的bot kernel不需要跟每一個機器人用戶端打交道,開發人員只需要專注在對談訊息邏輯的撰寫即可,也多了一個最基本的Web對談介面(WebChat)。

因此,有了這個概念之後,你就知道,最左邊的your bot’s web service,是一個我們要先建立好的WebSite,因此,建議開發人員可以先建立好一個對外連線的網站(用Azure WebSite是一個好的選擇)。要快速地建立這個WebSite,你可以下載一個VS2015的專案範本,位於:http://aka.ms/bf-bc-vstemplate
下載之後的檔案請放在 "%USERPROFILE%\Documents\Visual Studio 2015\Templates\ProjectTemplates\Visual C#\"

然後關閉Visual Studio 2015重新開啟,你會看到在C#專案範本底下多了這個:

請用它建立一個專案,你會發現結構上就是一個WebAPI Site,下圖是比較V3與V1版本之間的差異:

我們暫且不改這個網站中的內容,直接把網站publish到azure website上,成功佈署後,開啟網站你會看到底下這個畫面:

在這個畫面當中,你會發現它指示你現在可以去註冊一個bot了,因此,我們來完成剛才未完成的那個動作。

請回到註冊Bot(其實Bot Connector)的畫面,點選Register a bot,接著在底下出現的畫面中,請填入下圖中標示1與2的Name與bot handle,注意那個『bot handle』基本上是一個唯一識別字,代表你的Bot,一旦設定後,是不能改的:

另外底下Configration區塊也要填寫:

請留意上面標示3的位置,這個網站就是剛才我們發佈到azure website上的網站,請在網址後面加上/api/messages,並且把網址改為https,填入上面欄位中。

接著,請按下上圖中的Create microsoft app id…按鈕,在出現的畫面中,取得App ID(應用程式識別碼),然後,按下底下的產生密碼以繼續:
這時,頁面會跳出一個視窗,要你記得一組密碼,該密碼只會顯示一次,請務必複製起來。

完成上述動作後,頁面會跳回bot註冊畫面,這時候你會看到App ID已經自動幫我們放上去了:

接著,請填妥下圖中打*號的欄位,請特別注意,其中底下兩個URL必須是以https開頭,否則你會收到意外的驚喜(驚喜? 就是註冊失敗的Exception message)。

一切都設定完成之後,請打勾頁面最下方的checkbox,並按下Register按鈕:

如果順利,你會看到底細下畫面:

這樣就建立完成了。

完成後,請進入該bot的管理後台,你會發現v3的版本很貼心的幫我們自動設定了Skype用戶端,原本的Web Chat用戶端自然也還在:
這時候別急,找出剛才我們發佈到azure的網站原始程式碼,修改web.config中的一些參數:

還記得嗎?

上面的第三個參數,是剛才跳出視窗時,要你記下的那個密碼。而第二個參數則是App ID(guid),而第一個參數則是你的bot ID。

設定完成之後,請把這一版的WebSite重新佈署上去。然後在bot管理站台按下test按鈕:

如果一切正常,會出現 Endpoint authorization succeeded 。
這時候,我們可以開始來測試bot,請在channels的地方,點選WebChat旁邊的Edit:

畫面會進入到WebChat的設定畫面,請按下『Regenerate…』按鈕,你會發現secret被產生:

產生了Secret之後,你可以回到bot後台主畫面,在主畫面中,可以點選 get bot embed codes:

先測試WebChat,你可以直接複製embed template中的html到你的網站上,該html是一段iframe標記,執行結果如下:

你會發現你的bot就被運行起來了。裡面的對話是哪來的呢?還是得我們先前透過Visual Studio建立的WebAPI project嗎?對話就是底下的這段程式碼所產生的:

當然,如果你修改其中的對話邏輯,則bot的反應也會隨之修改(我們後面再來介紹如何透過formflow或是LUIS來讓bot學會與用戶對話。)
採用v3版本的bot,有一個好處是,MS順便建立了skype bot,你可以從後台看到一個link:


點選之後,會出現底下畫面:

這個畫面是讓你的用戶(end users),跟你的skype bot做朋友用的,你可以把該連結貼到你的網站上,當用戶進入此畫面,按下Add to Contacts,如果用戶有安裝Skype,就會出現底下畫面:

當用戶按下『是』,skype應用程式就會被喚起,出現:

按下上圖中的『新增到聯絡人』之後,你的用戶就可以跟你的bot對話了:

你會發現skype bot與web chat的回應是完全一樣的,當然,這是因為它用的是同樣的一個WebSite,同樣的對談邏輯。bot connector(下圖中間)作為一個中間人的角色,幫我們把訊息傳送到skype/WebChat的channel中(下圖右方),我們的bot只是一組對話邏輯(下圖左方)

如此一來,你就更明白一開始我們說,跟MS註冊一個bot,嚴格說起來是註冊一個bot connector的意思了吧?

OK,我們幾乎沒有寫任何程式,完成了Web bot和skype bot的建立了,後面我們再來談談,如何讓bot變的聰明一點,知道用戶的意思

2016年8月21日 星期日

Microsoft Cognitive Services (2) - 使用Vision API進行OCR

既然是Vision,那範圍當然不只包含辨識圖片,或是找到照片中的人臉,Vision API還可以幫你達成OCR的功能。實際測試的結果,如果是印刷體的文字,辨識率算是不錯了,而且支援多種語文,包含中文,就很值得大家關注一下了。

底下是實際辨識一張slide的結果,你會發現不管是標題或是內文,辨識度都非常好(不過也是因為是標準清晰的印刷體的原因):

別小看Vision API的OCR功能,在同一個圖檔中,有多行不同角度傾斜的文字,在一定的範圍內,Vision API依舊可以辨識出來,它是一個區塊一個區塊去辨識的,重點是,程式寫起來一點都不複雜。

底下這段WPF的Code,就是按下上圖中的『選擇文字照片』之後,所執行的完整程式碼:

//抓取key
var VisionApiKey = textBoxkey.Text.Trim();
//讀取圖檔
var openDlg = new Microsoft.Win32.OpenFileDialog();
//圖檔過濾類型
openDlg.Filter = "JPEG Image(*.jpg)|*.jpg|*.png|*.png";
bool? result = openDlg.ShowDialog(this);
//沒選檔案
if (!(bool)result)   return;

//取得選擇的檔案名稱
string filePath = openDlg.FileName;
//OCR OcrResults
OcrResults OcrResults;
//建立VisionServiceClient
var visionClient = new Microsoft.ProjectOxford.Vision.VisionServiceClient(VisionApiKey); 
using (var fs = new FileStream(filePath, FileMode.Open))
{
    this.textBox.Text = "辨識中...";
    //以繁體中文辨識
    OcrResults = await visionClient.RecognizeTextAsync(fs, LanguageCodes.ChineseTraditional);
    this.textBox.Text = "";
}

this.textBox.Text = "";
//抓取每一區塊的辨識結果
foreach (var Region in OcrResults.Regions)
{
    //抓取每一行
    foreach (var line in Region.Lines)
    {
        //抓取每一個字
        foreach (var Word in line.Words)
        {
            //顯示辨識結果
            this.textBox.Text += Word.Text;
        }
        //加換行
        this.textBox.Text += "\n";
    }
}

//顯示原始圖片
Uri fileUri = new Uri(filePath);
BitmapImage bitmapSource = new BitmapImage();
bitmapSource.BeginInit();
bitmapSource.CacheOption = BitmapCacheOption.None;
bitmapSource.UriSource = fileUri;
bitmapSource.EndInit();
image1.Source = bitmapSource;

幾乎不用解釋,非常的簡單。

建立了VisionServiceClient之後,將讀入的stream傳遞給RecognizeTextAsync方法,並且指定要用繁體中文辨識。

結果以OcrResults這個類別的物件回傳,其中OcrResults.Regions是辨識出的每一個文字區塊,接著以Region.Lines和line.Words抓取到每一行和每一個字,就這樣,辨識出來囉…

超容易的吧!