Alan Tsai 的學習筆記


學而不思則罔,思而不學則殆,不思不學則“網貸” 為現任微軟最有價值專家 (MVP)、微軟認證講師 (MCT) 、Blogger、Youtuber:記錄軟體開發的點點滴滴 著重於微軟技術、C#、ASP .NET、Azure、DevOps、Docker、AI、Chatbot、Data Science

[chatbot + AI = 下一代操作模式][28]整合Custom Vision到chatbot - 拍照就可以識別價錢

[chatbot + AI = 下一代操作模式][28]整合Custom Vision到chatbot - 拍照就可以識別價錢.jpg
圖片來源:https://pixabay.com/en/books-spine-colors-pastel-1099067/ 

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

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

這篇的程式碼github頁面是alantsai-samples/mhat-hotelbot:blog/chapter-28

取得預測模型的URL網址

Custom Vision和其他一樣,也是透過REST API的方式在呼叫,同OCR一樣,有兩種模式:

  1. 可以傳一張圖片url網址
  2. 可以傳一張圖片的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}

  1. {projectId} - 這個稍後會使用到,要記錄下來
  2. {method} - 兩種,如果是網址類型就是url,檔案則是file。這個之後不會用到沒有記錄沒關係

調整程式碼整合Custom Vision服務

程式碼的部分修改和Computer Vision很類似,不過這次沒有現成的SDK可以使用,因此Service裡面需要做比較多的事情。

整個的調整步奏如下:

  1. 建立一個Custom Vision的Service
  2. 建立一個處理Custom Vision的Dialog
  3. 調整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;
}

這邊要注意一下,網址寫死了使用south central us的REST endpoint,如果有用不同地區的key這個部分要注意。

接下來,在這個class裡面建立一個method叫做GetTag,這個方法有兩個版本:

  1. 接受string - 代表url類型圖片的服務
  2. 接受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方法:

  1. GetMostPossibleTagName - 回傳的結果包含多個tag,我們只要判斷最高那個即可。這個方法處理這個事情
  2. 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);
}

測試結果

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

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。


如果文章對您有幫助,就請我喝杯飲料吧
街口支付QR Code
街口支付QR Code
台灣 Pay QR Code
台灣 Pay QR Code
Line Pay 一卡通 QR Code
Line Pay 一卡通 QR Code
街口支付QR Code
支付寶QR Code
街口支付QR Code
微信支付QR Code
comments powered by Disqus