Alan Tsai 的學習筆記


學而不思則罔,思而不學則殆,不思不學則“網貸” 記錄軟體開發的點點滴滴 著重於微軟技術、網頁開發、DevOps、C#, Asp .net Mvc、Azure、AI、Chatbot、Docker、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
街口支付QR Code
支付寶QR Code
街口支付QR Code
微信支付QR Code
comments powered by Disqus