iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 28
0
Everything on Azure

使用 Microsoft Conversational AI Tools - 打造新时代的UI界面系列 第 28

[28]整合Custom Vision到chatbot - 拍照就可以識別價錢

在上一篇([27]Custom Vision - 自己的Model自己Train 建立圖片的分類模型)瞭解了如何使用Custom Vision去train一個圖片的classifier模型,并且用了一些測試照片去測試模型的準確度。

是時候把這個功能整合到chatbot裡面了。這一篇將來實作整合進入chatbot的功能并且實現上篇提到的情景 - 透過拍照就可以知道這個飲料是多少錢。

取得預測模型的URL網址
Custom Vision和其他一樣,也是透過REST API的方式在呼叫,同OCR一樣,有兩種模式:

可以傳一張圖片url網址
可以傳一張圖片的binary資訊
首先進入到customvision.ai,然後進入測試的專案,選擇上面的Peformance,然後按下Make Default:

chrome_2018-08-07_19-21-59.png
把目前Iteration 1的設定為預設模型
設定為Default會影響到等一下取得的Prediction Url,在之後篇幅會有更詳細介紹,目前先知道爲了後面程式好接,設定為Default比較方便。
設定好了之後,然後找到Prediction Url:
chrome_2018-08-07_19-17-52.png
Prediction API網址取得的方式
從上面的截圖,可以看到兩個模式,傳入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;
}
C#
這邊要注意一下,網址寫死了使用south central us的REST endpoint,如果有用不同地區的key這個部分要注意。
接下來,在這個class裡面建立一個method叫做GetTag,這個方法有兩個版本:

接受string - 代表url類型圖片的服務
接受stream - 代表實體檔案上傳的服務
public async Task 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 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;

}
C#
在這兩個方法裡面,有使用到兩個Helper方法:

GetMostPossibleTagName - 回傳的結果包含多個tag,我們只要判斷最高那個即可。這個方法處理這個事情
GetStreamAsByteArray - 把Stream轉換成爲byte[],方便呼叫服務
private string GetMostPossibleTagName(string json)
{
var model = JsonConvert.DeserializeObject
(json);

return $"{model.predictions.
	FirstOrDefault().tagName}";

}

private byte[] GetStreamAsByteArray(Stream stream)
{
var ms = new MemoryStream();
stream.CopyTo(ms);
return ms.ToArray();
}
C#
最後,當服務呼叫完了,回傳的是一個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; }
}
C#
建立一個處理Custom Vision的Dialog
接下來建立一個Dialog叫做DrinkPriceCheckerDialog,這個Dialog作用很簡單,把圖片透過CustomVisionService取得判斷,然後在返回物品名稱以及價錢。

首先是Dialog裡面的一些boilerplate的程式碼,這個Dialog需要傳入CustomVisionService,然後進入的時候有一段文字説明:

public async Task StartAsync(IDialogContext context)
{
await context.PostAsync
("請上傳飲料圖片或者圖片的網址");

context.Wait(MessageReceivedAsync);

}
C#
接下來是重點的邏輯,呼叫CustomVisionService來判斷屬於什麽飲料,然後在把價錢返回去:

private async Task MessageReceivedAsync
(IDialogContext context,
IAwaitable 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);

}
C#
這邊要注意,在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 result)
{
var finalResult = await result;

await context.PostAsync(finalResult);

context.Wait(MessageReceived);

}
C#
測試結果
到目前爲止,整個調整就完成了,接下來就是進行測試的時候。

botframework-emulator_2018-08-08_20-54-21.png
圖片識別出雪碧
可以看到,chatbot現在可以識別出圖片分類,并且正確的分類成爲了雪碧。

到這邊,這篇就結束了,但是這個其實有很多可以加强的地方,舉例來説,回傳的内容其實可以考慮之前介紹過的Recipt Card模式,至少會漂亮很多。

另外,如果這個有部署到某個channel,例如FB,其實可以真的去實地拍照測試,很好玩的哦。

結語
這篇介紹了如何把Custom Vision上面的Model整合到chatbot裡面,這不止讓操作體驗提升,也避免了描述飲料名稱不好查的問題(例如,假設有國外客戶,可能用sprite來查,但是中國人可能是雪碧,透過圖片,所有人輸入方式一樣)。

所謂生兒容易,養兒難,軟件開發也一樣。接下來怎麽持續精進這個Model讓他更加準確呢?下篇([29]維護Custon Vision Model - 使用歷史查詢記錄做訓練以及如何版控)來看看如何持續維護Custom Vision。


上一篇
[27]Custom Vision - 自己的Model自己Train 建立圖片的分類模型
下一篇
[29]維護Custon Vision Model - 使用歷史查詢記錄做訓練以及如何版控
系列文
使用 Microsoft Conversational AI Tools - 打造新时代的UI界面30

尚未有邦友留言

立即登入留言