在上一篇([27]Custom Vision - 自己的Model自己Train 建立圖片的分類模型)瞭解了如何使用Custom Vision去train一個圖片的classifier模型,并且用了一些測試照片去測試模型的準確度。
是時候把這個功能整合到chatbot裡面了。這一篇將來實作整合進入chatbot的功能并且實現上篇提到的情景 - 透過拍照就可以知道這個飲料是多少錢。
取得預測模型的URL網址
Custom Vision和其他一樣,也是透過REST API的方式在呼叫,同OCR一樣,有兩種模式:
- 可以傳一張圖片url網址
- 可以傳一張圖片的binary資訊
首先進入到customvision.ai,然後進入測試的專案,選擇上面的Peformance
,然後按下Make Default
:
Prediction Url
:
從上面的截圖,可以看到兩個模式,傳入image url或者是image file的方式。這兩個方式的網址在輸入框裡面,把他們複製下來即可。剩下紅色字的部分都是需要設定在Header或者Body的範例内容。
Prediction-Key
很重要,不要讓別人知道。
這一步做完了之後,得到的url會類似:
https://southcentralus.api.cognitive.microsoft.com/customvision/v2.0/Prediction/
{projectId}/{method}
- {projectId} - 這個稍後會使用到,要記錄下來
- {method} - 兩種,如果是網址類型就是
url
,檔案則是file
。這個之後不會用到沒有記錄沒關係
調整程式碼整合Custom Vision服務
程式碼的部分修改和Computer Vision很類似,不過這次沒有現成的SDK可以使用,因此Service裡面需要做比較多的事情。
整個的調整步奏如下:
- 建立一個Custom Vision的Service
- 建立一個處理Custom Vision的Dialog
- 調整LUIS加入查價錢的intent以及修改dialog
建立一個Custom Vision的Service
首先建立出一個Service叫做:CustomVisionService
,這個class目的是把Custom Vision的REST Api包住,方便在C#呼叫。
首先,這個class能夠注入Project Id以及Prediction Key:
public string ProjectId { get; }
public string PredictionKey { get; }
public string PredictionBaseUrl
{
get
{
return $"https://southcentralus.api.cognitive.microsoft.com/customvision/v2.0/Prediction/{ProjectId}";
}
}
public string PredictionImageUrl
{
get
{
return $"{PredictionBaseUrl}/image";
}
}
public string PredictionImageUrlUrl
{
get
{
return $"{PredictionBaseUrl}/url";
}
}
public CustomVisionService(string projectId, string predictionKey)
{
ProjectId = projectId;
PredictionKey = predictionKey;
}
接下來,在這個class裡面建立一個method叫做GetTag
,這個方法有兩個版本:
- 接受string - 代表url類型圖片的服務
- 接受stream - 代表實體檔案上傳的服務
public async Task<string> GetTag(string imageUrl)
{
var result = string.Empty;
var client = new HttpClient();
client.DefaultRequestHeaders
.Add("Prediction-Key", PredictionKey);
string url = PredictionImageUrlUrl;
HttpResponseMessage response;
using (var content =
new StringContent($"{{ \"Url\": \"{imageUrl}\"}}"))
{
content.Headers.ContentType =
new MediaTypeHeaderValue("application/json");
response = await client.PostAsync(url, content);
var json = await response.Content.ReadAsStringAsync();
result = GetMostPossibleTagName(json);
}
return result;
}
public async Task<string> GetTag(Stream stream)
{
var result = string.Empty;
var client = new HttpClient();
client.DefaultRequestHeaders
.Add("Prediction-Key", PredictionKey);
string url = PredictionImageUrl;
HttpResponseMessage response;
byte[] byteData = GetStreamAsByteArray(stream);
using (var content = new ByteArrayContent(byteData))
{
content.Headers.ContentType =
new MediaTypeHeaderValue("application/octet-stream");
response = await client.PostAsync(url, content);
var json = await response.Content.ReadAsStringAsync();
result = GetMostPossibleTagName(json);
}
return result;
}
在這兩個方法裡面,有使用到兩個Helper方法:
GetMostPossibleTagName
- 回傳的結果包含多個tag,我們只要判斷最高那個即可。這個方法處理這個事情GetStreamAsByteArray
- 把Stream轉換成爲byte[],方便呼叫服務
private string GetMostPossibleTagName(string json)
{
var model = JsonConvert.DeserializeObject
<PredicationResponse>(json);
return $"{model.predictions.
FirstOrDefault().tagName}";
}
private byte[] GetStreamAsByteArray(Stream stream)
{
var ms = new MemoryStream();
stream.CopyTo(ms);
return ms.ToArray();
}
最後,當服務呼叫完了,回傳的是一個JSON的内容,因此有個C#的class對應這個JSON,稱之爲PredicationResponse
:
public class PredicationResponse
{
public string id { get; set; }
public string project { get; set; }
public string iteration { get; set; }
public DateTime created { get; set; }
public Prediction[] predictions { get; set; }
}
public class Prediction
{
public float probability { get; set; }
public string tagId { get; set; }
public string tagName { get; set; }
}
建立一個處理Custom Vision的Dialog
接下來建立一個Dialog叫做DrinkPriceCheckerDialog
,這個Dialog作用很簡單,把圖片透過CustomVisionService
取得判斷,然後在返回物品名稱以及價錢。
首先是Dialog裡面的一些boilerplate的程式碼,這個Dialog需要傳入CustomVisionService
,然後進入的時候有一段文字説明:
public async Task StartAsync(IDialogContext context)
{
await context.PostAsync
("請上傳飲料圖片或者圖片的網址");
context.Wait(MessageReceivedAsync);
}
接下來是重點的邏輯,呼叫CustomVisionService來判斷屬於什麽飲料,然後在把價錢返回去:
private async Task MessageReceivedAsync
(IDialogContext context,
IAwaitable<IMessageActivity> result)
{
var CustomVisionServiceInstance =
new CustomVisionService
(ConfigurationManager
.AppSettings["CustomVision.ProjectId"],
ConfigurationManager
.AppSettings["CustomVision.Key"]);
var messageResult = await result;
var connector = messageResult.GetConnector();
var finalResult = string.Empty;
var imageAttachment = messageResult
.Attachments
?.FirstOrDefault
(a => a.ContentType.Contains("image"));
if (imageAttachment != null)
{
using (var stream = await connector
.GetImageStream(imageAttachment))
{
finalResult =
await CustomVisionServiceInstance
.GetTag(stream);
}
}
else if (Uri.IsWellFormedUriString
(messageResult.Text, UriKind.Absolute))
{
finalResult =
await CustomVisionServiceInstance
.GetTag(messageResult.Text);
}
switch (finalResult)
{
case "coke":
finalResult = "可樂:20元";
break;
case "sprite":
finalResult = "雪碧:10元";
break;
case "pepsi":
finalResult = "百事可樂:50元";
break;
default:
finalResult = "找不到對應的飲料,請重新拍照";
break;
}
context.Done(finalResult);
}
Web.config
,要增加AppSettings用來設定Custom Vision的ProjectId以及Key
調整LUIS加入查價錢的intent以及修改dialog
邏輯都准備好了,接下來就是整合的部分。
首先,先到LUIS增加一個Intent叫做CheckDrinkPrice
。
再來,調整RootLuisDialog
加入這個intent的處理:
[LuisIntent("CheckDrinkPrice")]
public Task CheckDrinkPrice
(IDialogContext context, LuisResult result)
{
context.Call(new DrinkPriceCheckerDialog(),
CheckDrinkPriceAfterAsync);
return Task.CompletedTask;
}
private async Task CheckDrinkPriceAfterAsync
(IDialogContext context
, IAwaitable<string> result)
{
var finalResult = await result;
await context.PostAsync(finalResult);
context.Wait(MessageReceived);
}
測試結果
到目前爲止,整個調整就完成了,接下來就是進行測試的時候。
可以看到,chatbot現在可以識別出圖片分類,并且正確的分類成爲了雪碧。
到這邊,這篇就結束了,但是這個其實有很多可以加强的地方,舉例來説,回傳的内容其實可以考慮之前介紹過的Recipt Card模式,至少會漂亮很多。
另外,如果這個有部署到某個channel,例如FB,其實可以真的去實地拍照測試,很好玩的哦。
結語
這篇介紹了如何把Custom Vision上面的Model整合到chatbot裡面,這不止讓操作體驗提升,也避免了描述飲料名稱不好查的問題(例如,假設有國外客戶,可能用sprite來查,但是中國人可能是雪碧,透過圖片,所有人輸入方式一樣)。
所謂生兒容易,養兒難,軟件開發也一樣。接下來怎麽持續精進這個Model讓他更加準確呢?下篇([29]維護Custon Vision Model - 使用歷史查詢記錄做訓練以及如何版控)來看看如何持續維護Custom Vision。