場景:Bob同窗有一天在網上看到了一張建築物的圖片,大發感慨:"好漂亮啊!這是哪裏?我要去親眼看看!"Bob同窗不想問別人,好笑的自尊心讓他以爲這確定是個著名的建築,若是本身不知道多丟臉!怎麼解決Bob同窗的煩惱呢?程序員
咱們看看微軟認知服務是否能幫助到Bob同窗,打開這個連接:web
https://azure.microsoft.com/zh-cn/services/cognitive-services/computer-vision/正則表達式
向下卷滾屏幕,到"識別名人和地標"部分,在"圖像URL"編輯框裏輸入了這張圖片的網絡地址,而後點擊"提交",一兩秒後,就能看到關於這張圖片的文字信息了(見下圖),原來這個建築叫作"Space Needle"!可是呢,不太人性化,由於是JSON文件格式的,幸虧Bob同窗是個程序員,Bob同窗想把這個場景作成一個實際的應用,以幫助他人解決相似問題。編程
Bob同窗剛學習了微軟認知服務的應用教程,因而打開Windows 10 PC,啓動VS2017,安裝了Visual Studio Tools for AI後,先在Server Explorer->AI Tools->Azure Cognitive Services上點擊鼠標右鍵,Create New Cognitive Service,API Type選擇ComputerVision (若是已經有了就不須要重複申請了),獲得了Key和Endpoint,按照《漫畫翻譯篇》教程所講述的過程,照貓畫虎,花了一兩個小時,就把應用作好了。json
開發技術文檔在這個連接裏面。設計模式
目前Bob的同窗的應用架構是這樣的:api
(上圖中右側的框圖內的文字是「地標識別」,下同) 安全
Bob同窗很滿意地試着本身的做品,長城,天安門,故宮……都能認出來!可是,Bob同窗突然想到,若是出門在外遇到一個漂亮建築,沒有PC,只有手機怎麼辦?因而Bob同窗又啓動了VS2017,建立了一個Xamarin項目,重用了PC上的code,把這個場景搞定了:拿起Android或者iOS手機,對着建築物一框,幾秒後就會有結果返回,告訴用戶眼前的這個建築叫什麼名字。太方便啦!服務器
因此,Bob同窗的應用架構進化了一些:網絡
Bob同窗用手機給不少同窗們安裝後顯擺了幾天,有人問他:"Space Needle是啥?"
"這個……這個……哦!你能夠在Bing上搜索一下啊!"
"你的程序能不能順便幫咱們搜索一下呢?"
"嗯……啊……固然啦!"硬着頭皮說了這句話後,Bob同窗趕忙回去查微軟認知服務的網站了。Bingo! 在這裏了:
https://azure.microsoft.com/zh-cn/services/cognitive-services/bing-entity-search-api/
與前面的教程裏描述的相似,申請了搜索服務後,也獲得了Endpoint和Key,照貓畫虎地把客戶端改了一下,增長了搜索服務的功能,銜接到了地標識別邏輯的後面,也就是把地標識別的結果"Space Needle"做爲關鍵字傳送給實體搜索服務,而後再把結果展現出來。
注意這裏要申請的API在Bing.Search.v7裏面,技術文檔在這個連接裏面。
因而Bob同窗的應用架構變成了這個樣子:
(上圖中右側的框圖內的文字是「實體搜索」,下同)
這個圖的鏈接線看着好奇怪,黃色的線爲何不鏈接到左側的客戶端上呢?這裏特地這樣畫,爲了表示黃色的鏈接(REST API調用)是接在藍色的鏈接以後的,有依賴關係。在下一個場景裏,你們會看到更復雜的例子。
在一陣手忙腳亂的部署以後,全部的同窗的手機均可以使用這個新App了,Bob同窗很自豪。這時,學習委員走過來了(也是體育課表明),問Bob:"出門旅遊的機會很少,我想用這個App作更多的平常的事情,好比掃一張照片,就能知道這個明星的名字和背景資料,或者是照一件衣服就能知道在哪裏買,還有看到一個電話號碼後,想用手機掃一下就能記錄下來……這些能辦到嗎?"
Bob同窗邊聽邊鎮靜地點頭,其實後背都溼透了,嘴上不能服軟:"我回去想一想辦法吧!"
Bob同窗翻閱了微軟認知服務的全部技能,在紙上畫了一個草圖,來解決學習委員的問題:
(上圖中右側的框圖內的文字是「名人識別」,下同)
同時有三根藍線都從同一個客戶端鏈接到不一樣的認知服務上,是由於客戶端程序並不知道要識別的物體是建築物呢,仍是人臉呢,或是電話號碼呢?須要一個個的去嘗試調用三個API,若是返回有效的結果,就表明識別出了該實體的類型。
畫完圖後,原本覺得會輕鬆的Bob同窗,突然發現他須要不斷更新三個客戶端的代碼:PC,Android,iOS,來知足更多的學習委員的須要(如同右側那個上下方向的箭頭同樣是可擴充的),而後再分別發佈出去!而且他意識到了另一個問題:每一個客戶端須要訪問認知服務四次才能完成這個場景!不但網絡速度對用戶體驗形成了影響,並且流量就是錢啊!若是未來須要支持更多的識別類型,鏈接線的增加速率將會是幾何級別的!
My Omnipotent God!Tell Me How!
Bob同窗想起了剛買到的《構建之法》第三版,仔細閱讀了第9,10,11三章,明白了一些基本的概念:
"我要重構!"房間裏響起了Bob同窗的吶喊聲,把隔壁鄰居嚇了一跳:"這小夥子是否是又失戀了?"
小提示:需求的"演進"與"變化"是兩回事兒,不要混爲一談來掩蓋項目經理對需求的分析與把握的不足。簡單地舉例來講,當項目經理說"地標識別看上去不多有人用,廢掉吧,我們作個名人識別",這個屬於需求變化。
微軟認知服務應用方式有兩大類:
第一種模式很好理解:微軟認知服務7x24小時在雲端提供服務,開發者在智能手機或者PC上編寫客戶端應用程序,調用REST API直接訪問雲端。可是這種模式有一些潛在的問題,如:
不管客戶端有多少,依賴的認知服務有多少,其實仍是下圖所示的模式:
目前Bob同窗就是使用這種方式,來不斷演進他的應用,終於遇到了棘手的問題。
爲何呢?由於客戶端一旦發佈到用戶手裏,對發佈者來講就比較被動了,須要很是當心地維護升級,每次都要全面測試,測試點多而複雜。即便有應用商店能夠幫助發佈,但要把全部用戶都升級到最新版本,仍是須要很長時間的,這意味着你還須要向後兼容。
第二種模式能夠用簡單的圖來表示:
有規模的商業化應用,通常都採用這種模式搭建應用架構,以便獲得如下好處:
拉個表格,一目瞭然:
直接訪問模式 |
中間服務層模式 |
|
客戶端代碼 |
量大,邏輯複雜 |
量小,邏輯簡單 |
發佈與維護 |
密集,改一點兒東西都須要從新發布新版本 |
中間層服務能屏蔽大量邏輯,不須要在客戶端代碼中體現 |
客戶端與認知服務的耦合度 |
極高 |
很低 |
客戶端與認知服務的通訊量 |
頻繁,屢次 |
單次 |
對認知服務密鑰的保護 |
低,用Fiddler就能夠"看到"認知服務密鑰 |
高,把費德勒叫來也不行 |
服務器端代碼 |
無 |
有 |
多種客戶端支持 |
複雜 |
簡單 |
若是有了中間服務層,客戶端的工做就簡化到只作與中間服務層通訊,提交請求,接收數據,用戶交互等等,而複雜的商業邏輯,能夠在中間服務層實現。並且在更新業務邏輯的時候,大多數狀況下,只須要修改中間服務層的代碼,無需更新客戶端。
對於多種客戶端的支持問題,用微軟VS2017提供的跨平臺Xamarin架構能夠解決,開發者只須要寫C#程序,就能夠把應用部署在Windows/Android/iOS設備上,一套代碼搞定。
中間層服務也不是十全十美,帶來的問題有二:1)須要雲端支持,要花錢的;2)圖片傳輸的過程會發生兩次,第一次是從客戶端到中間層,第二次是中間層到微軟認知服務,這樣會增長網絡時間上的開銷。可是有個好消息是第二次傳輸所花費的時間要比第一次小一個數量級,由於是服務器對服務器的通訊,若是你本身的服務器也放在Azure上,那麼和微軟認知服務的服務器就可能在一個大機房裏了,局域網的速度!而且,這個開銷與客戶端屢次訪問服務器相比,也是佔優的選擇,因此你們能夠在有條件的狀況下儘可能使用第二種方式作商業應用。
若是關注於對認知服務的使用,也能夠用另一種分類方式:
好比上面的最後的場景,其實是第四種方式:先並行使用了地標識別、名人識別、OCR,而後又串行使用了實體搜索服務。
咱們來幫助Bob同窗從新設計一下他的應用架構:
上圖只是個粗略的架構,中間服務層具體如何實現呢?
咱們常聽到的一句話是"這個問題你只要充值就能解決了" 沒錯,作信仰充值:先安裝Visual Studio 2017 and Tools for AI,再接着往下看。
環境要求:
基本步驟:
下面咱們展開第4步作詳細說明。
在VS2017中建立一個新項目,選擇Web->ASP.NET Core Web Application,以下圖:
給項目取個名字叫作"CognitiveMiddlewareService",Location本身隨便選,而後點擊OK進入下圖:
在上圖中選擇"API",不要動其餘任何選項,點擊OK,VS一陣忙碌以後,就會生成下圖的解決方案:
這是一個最基本的ASP.NET Core Web App的框架代碼,咱們將會在這個基礎上增長咱們本身的邏輯。在寫代碼以前,咱們先一塊兒搞清楚兩個關於ASP.NET Core框架的基本概念。
ASP.NET Core 支持依賴關係注入 (DI) 軟件設計模式,這是一種在類及其依賴關係之間實現控制反轉 (IoC) 的技術,原文連接在這裏。簡單的說就是:
咱們在後面的代碼中會有進一步的說明。
框架提供了一種機制,能夠經過註冊IHttpClientFactory用於建立HttpClient實例,這種方式帶來如下好處:
以上是原文提供的解釋,連接在這裏。可能比較難理解,但坊間一直流傳着HttpClient不能釋放的問題,因此用IHttpClientFactory應該至少能夠解決這個問題。
可是在使用它以前,咱們須要安裝一個NuGet包。在解決方案的名字上點擊鼠標右鍵,在出現的菜單中選擇"Manage NuGet Packages…",在出現的以下窗口中,輸入"Microsoft.extensions.http",而後安裝Microsoft.Extensions.Http包:
安裝完畢後,須要在Startup.cs文件裏增長依賴注入:services.AddHttpClient()。
先在生成好的框架代碼的基礎上,創建下圖所示的文件夾:
Controllers是基礎框架帶的文件夾,不須要本身建立。
建立這些文件夾的目的,是讓咱們本身可以縷清邏輯,寫代碼時注意調用和被調用的關係,用必要的層次來體現軟件的抽象。以本案例來講,模塊劃分與層次抽象應該以下圖所示(下圖中帶箭頭的實線表示調用關係):
藍色的層,也就是CognitiveServices文件夾,包含了兩個訪問認知服務的基礎功能:VisionService和EntitySearchService。
它們返回了最底層的結果:VisionResult和EntityResult。這一層的每一個服務,只專一於本身的網絡請求與接收結果的任務,無論其它的事情。若是認知服務編程接口有變化,只修改這一層的代碼。
黃色的層,也就是MiddlewareService文件夾,是咱們本身包裝認知服務的邏輯層,在這個層中的代碼,每個服務都是用串行方式訪問認知服務的:在用第一個輸入(假設是圖片)獲得第一個認知服務的返回結果後(假設是文字),再把這個返回結果輸入到第二個認知服務中去,獲得內容更豐富的結果。
它們返回了集成後的結果:LandmarkResult和CelebrityResult,這兩個結果的定義已經對認知服務返回的結果進行了進一步的抽象和隔離,其目的是讓後面的邏輯代碼只針對這一層的抽象進行處理,沒必要考慮更底層的數據結構。
綠色的層,也就是Processors文件夾,是包裝業務邏輯的代碼,在本層中作任務分發,用並行方式同時訪問兩個以上的認知服務,將返回的結果聚合在一塊兒,並根據須要進行排序,最後生成要返回的結果AggregatedResult。
在這個文件夾中,咱們須要添加如下文件:
IVisionService.cs - 訪問影像服務的接口定義,須要依賴注入
using System.IO; using System.Threading.Tasks; namespace CognitiveMiddlewareService.CognitiveServices { public interface IVisionService { Task<Landmark> RecognizeLandmarkAsync(Stream imgStream); Task<Celebrity> RecognizeCelebrityAsync(Stream imgStream); } }
VisionService.cs - 訪問影像服務的邏輯代碼
using Newtonsoft.Json; using System; using System.IO; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; namespace CognitiveMiddlewareService.CognitiveServices { public class VisionService : IVisionService { const string LandmarkEndpoint = "https://eastasia.api.cognitive.microsoft.com/vision/v2.0/models/landmarks/analyze"; const string CelebrityEndpoint = "https://eastasia.api.cognitive.microsoft.com/vision/v2.0/models/celebrities/analyze"; const string Key1 = "0e290876aed45d69f6fb97bb621f71"; const string Key2 = "9799f09b87e4be6b2be132309b8e57"; private readonly IHttpClientFactory httpClientFactory; public VisionService(IHttpClientFactory cf) { this.httpClientFactory = cf; } public async Task<Landmark> RecognizeLandmarkAsync(Stream imgStream) { VisionResult result = await this.MakePostRequest(LandmarkEndpoint, imgStream); if (result?.result?.landmarks?.Length > 0) { return result?.result?.landmarks[0]; } return null; } public async Task<Celebrity> RecognizeCelebrityAsync(Stream imgStream) { VisionResult result = await this.MakePostRequest(CelebrityEndpoint, imgStream); if (result?.result?.celebrities?.Length > 0) { return result?.result?.celebrities[0]; } return null; } private async Task<VisionResult> MakePostRequest(string uri, Stream imageStream) { try { using (HttpClient httpClient = httpClientFactory.CreateClient()) { using (StreamContent streamContent = new StreamContent(imageStream)) { streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); using (var request = new HttpRequestMessage(HttpMethod.Post, uri)) { request.Content = streamContent; request.Headers.Add("Ocp-Apim-Subscription-Key", Key1); using (HttpResponseMessage response = await httpClient.SendAsync(request)) { if (response.IsSuccessStatusCode) { string resultString = await response.Content.ReadAsStringAsync(); VisionResult result = JsonConvert.DeserializeObject<VisionResult>(resultString); return result; } else { } } } return null; } } } catch (Exception ex) { return null; } } } }
小提示:上面的代碼中的Key1/Key2是不可用的,請用本身申請的Key和對應的Endpoint來代替。
VisionResult.cs – 認知服務返回的結果類,用於反序列化
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace CognitiveMiddlewareService.CognitiveServices { public class VisionResult { public Result result { get; set; } public string requestId { get; set; } } public class Result { public Landmark[] landmarks { get; set; } public Celebrity[] celebrities { get; set; } } public class Landmark { public string name { get; set; } public double confidence { get; set; } } public class Celebrity { public virtual string name { get; set; } public virtual double confidence { get; set; } } }
IEntitySearchService.cs – 訪問實體搜索服務的接口定義,須要依賴注入
using System.Threading.Tasks; namespace CognitiveMiddlewareService.CognitiveServices { public interface IEntitySearchService { Task<string> SearchEntityAsync(string query); } }
EntitySearchService.cs – 訪問實體搜索服務的邏輯代碼
using System.Diagnostics; using System.Net.Http; using System.Threading.Tasks; namespace CognitiveMiddlewareService.CognitiveServices { public class EntitySearchService : IEntitySearchService { const string SearchEntityEndpoint = "https://api.cognitive.microsoft.com/bing/v7.0/entities?mkt=en-US&q="; const string Key1 = "a0be81df8ad449481492a11107645b"; const string Key2 = "0803e4673824f9abb7487d8c3db6dd"; private readonly IHttpClientFactory httpClientFactory; public EntitySearchService(IHttpClientFactory cf) { this.httpClientFactory = cf; } public async Task<string> SearchEntityAsync(string query) { using (HttpClient hc = this.httpClientFactory.CreateClient()) { string uri = SearchEntityEndpoint + query; string jsonResult = await Helper.MakeGetRequest(hc, uri, Key1); Debug.Write(jsonResult); return jsonResult; } } } }
小提示:上面的代碼中的Key1/Key2是不可用的,請用本身申請的Key和對應的Endpoint來代替。
EntityResult.cs – 認知服務返回的結果類,用於反序列化
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace CognitiveMiddlewareService.CognitiveServices { public class EntityResult { public string _type { get; set; } public Querycontext queryContext { get; set; } public Entities entities { get; set; } public Rankingresponse rankingResponse { get; set; } } public class Querycontext { public string originalQuery { get; set; } } public class Entities { public Value[] value { get; set; } } public class Value { public string id { get; set; } public Contractualrule[] contractualRules { get; set; } public string webSearchUrl { get; set; } public string name { get; set; } public string url { get; set; } public Image image { get; set; } public string description { get; set; } public Entitypresentationinfo entityPresentationInfo { get; set; } public string bingId { get; set; } } public class Image { public string name { get; set; } public string thumbnailUrl { get; set; } public Provider[] provider { get; set; } public string hostPageUrl { get; set; } public int width { get; set; } public int height { get; set; } public int sourceWidth { get; set; } public int sourceHeight { get; set; } } public class Provider { public string _type { get; set; } public string url { get; set; } } public class Entitypresentationinfo { public string entityScenario { get; set; } public string[] entityTypeHints { get; set; } } public class Contractualrule { public string _type { get; set; } public string targetPropertyName { get; set; } public bool mustBeCloseToContent { get; set; } public License license { get; set; } public string licenseNotice { get; set; } public string text { get; set; } public string url { get; set; } } public class License { public string name { get; set; } public string url { get; set; } } public class Rankingresponse { public Sidebar sidebar { get; set; } } public class Sidebar { public Item[] items { get; set; } } public class Item { public string answerType { get; set; } public int resultIndex { get; set; } public Value1 value { get; set; } } public class Value1 { public string id { get; set; } } }
Helper.cs – 幫助函數
using Microsoft.AspNetCore.Http; using System; using System.IO; using System.Net.Http; using System.Threading.Tasks; namespace CognitiveMiddlewareService.CognitiveServices { public class Helper { public static byte[] GetBuffer(IFormFile formFile) { Stream stream = formFile.OpenReadStream(); MemoryStream memoryStream = new MemoryStream(); formFile.CopyTo(memoryStream); var buffer = memoryStream.GetBuffer(); return buffer; } public static MemoryStream GetStream(byte[] buffer) { if (buffer == null) { return null; } return new MemoryStream(buffer, false); } public static async Task<string> MakeGetRequest(HttpClient httpClient, string uri, string key) { try { using (var request = new HttpRequestMessage(HttpMethod.Get, uri)) { request.Headers.Add("Ocp-Apim-Subscription-Key", key); using (HttpResponseMessage response = await httpClient.SendAsync(request)) { if (response.IsSuccessStatusCode) { string jsonResult = await response.Content.ReadAsStringAsync(); return jsonResult; } } } return null; } catch (Exception ex) { return null; } } } }
在這個文件夾中,咱們須要添加如下文件:
ICelebrityService.cs – 包裝多個串行的認知服務來實現名人識別的中間服務層的接口定義,須要依賴注入
using System.Threading.Tasks; namespace CognitiveMiddlewareService.MiddlewareService { public interface ICelebrityService { Task<CelebrityResult> Do(byte[] imgData); } }
CelebrityService.cs – 包裝多個串行的認知服務來實現名人識別中間服務層的邏輯代碼
using CognitiveMiddlewareService.CognitiveServices; using Newtonsoft.Json; using System.Threading.Tasks; namespace CognitiveMiddlewareService.MiddlewareService { public class CelebrityService : ICelebrityService { private readonly IVisionService visionService; private readonly IEntitySearchService entityService; public CelebrityService(IVisionService vs, IEntitySearchService ess) { this.visionService = vs; this.entityService = ess; } public async Task<CelebrityResult> Do(byte[] imgData) { // get original recognized result var stream = Helper.GetStream(imgData); Celebrity celebrity = await this.visionService.RecognizeCelebrityAsync(stream); if (celebrity != null) { // get entity search result string entityName = celebrity.name; string jsonResult = await this.entityService.SearchEntityAsync(entityName); EntityResult er = JsonConvert.DeserializeObject<EntityResult>(jsonResult); if (er?.entities?.value.Length > 0) { // isolation layer: decouple data structure then return abstract result CelebrityResult cr = new CelebrityResult() { Name = er.entities.value[0].name, Description = er.entities.value[0].description, Url = er.entities.value[0].url, ThumbnailUrl = er.entities.value[0].image.thumbnailUrl, Confidence = celebrity.confidence }; return cr; } } return null; } } }
小提示:上面的代碼中,用CelebrityResult接管了實體搜索結果和名人識別結果的部分有效字段,以達到解耦/隔離的做用,後面的代碼只關心CelebrityResult如何定義的便可。
CelebrityResult.cs – 抽象出來的名人識別服務的返回結果
namespace CognitiveMiddlewareService.MiddlewareService { public class CelebrityResult { public string Name { get; set; } public double Confidence { get; set; } public string Url { get; set; } public string Description { get; set; } public string ThumbnailUrl { get; set; } } }
ILandmarkService.cs – 包裝多個串行的認知服務來實現地標識別的中間服務層的接口定義,須要依賴注入
using CognitiveMiddlewareService.CognitiveServices; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace CognitiveMiddlewareService.MiddlewareService { public interface ILandmarkService { Task<LandmarkResult> Do(byte[] imgData); } }
LandmarkService.cs – 包裝多個串行的認知服務來實現地標識別的中間服務層的邏輯代碼
using CognitiveMiddlewareService.CognitiveServices; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace CognitiveMiddlewareService.MiddlewareService { public class LandmarkService : ILandmarkService { private readonly IVisionService visionService; private readonly IEntitySearchService entityService; public LandmarkService(IVisionService vs, IEntitySearchService ess) { this.visionService = vs; this.entityService = ess; } public async Task<LandmarkResult> Do(byte[] imgData) { // get original recognized result var streamLandmark = Helper.GetStream(imgData); Landmark landmark = await this.visionService.RecognizeLandmarkAsync(streamLandmark); if (landmark != null) { // get entity search result string entityName = landmark.name; string jsonResult = await this.entityService.SearchEntityAsync(entityName); EntityResult er = JsonConvert.DeserializeObject<EntityResult>(jsonResult); // isolation layer: decouple data structure then return abstract result LandmarkResult lr = new LandmarkResult() { Name = er.entities.value[0].name, Description = er.entities.value[0].description, Url = er.entities.value[0].url, ThumbnailUrl = er.entities.value[0].image.thumbnailUrl, Confidence = landmark.confidence }; return lr; } return null; } } }
小提示:上面的代碼中,用LandmarkResult接管了實體搜索結果和地標識別結果的部分有效字段,以達到解耦/隔離的做用,後面的代碼只關心LandmarkResult如何定義的便可。
LandmarkResult.cs – 抽象出來的地標識別服務的返回結果
namespace CognitiveMiddlewareService.MiddlewareService { public class LandmarkResult { public string Name { get; set; } public double Confidence { get; set; } public string Url { get; set; } public string Description { get; set; } public string ThumbnailUrl { get; set; } } }
在這個文件夾中,咱們須要添加如下文件:
IProcessService.cs – 任務調度層服務的接口定義,須要依賴注入
using System.Threading.Tasks; namespace CognitiveMiddlewareService.Processors { public interface IProcessService { Task<AggregatedResult> Process(byte[] imgData); } }
ProcessService.cs – 任務調度層服務的邏輯代碼
using CognitiveMiddlewareService.MiddlewareService; using System.Collections.Generic; using System.Threading.Tasks; namespace CognitiveMiddlewareService.Processors { public class ProcessService : IProcessService { private readonly ILandmarkService landmarkService; private readonly ICelebrityService celebrityService; public ProcessService(ILandmarkService ls, ICelebrityService cs) { this.landmarkService = ls; this.celebrityService = cs; } public async Task<AggregatedResult> Process(byte[] imgData) { // preprocess // todo: create screening image classifier to get a rough category, then decide call which service // task dispatcher: parallelized run 'Do' // todo: put this logic into Dispatcher service List<Task> listTask = new List<Task>(); var taskLandmark = this.landmarkService.Do(imgData); listTask.Add(taskLandmark); var taskCelebrity = this.celebrityService.Do(imgData); listTask.Add(taskCelebrity); await Task.WhenAll(listTask); LandmarkResult lmResult = taskLandmark.Result; CelebrityResult cbResult = taskCelebrity.Result; // aggregator // todo: put this logic into Aggregator service AggregatedResult ar = new AggregatedResult() { Landmark = lmResult, Celebrity = cbResult }; return ar; // ranker // todo: if there have more than one result in AgregatedResult, need give them a ranking // output generator // todo: generate specified JSON data, such as Adptive Card } } }
小提示:你們能夠看到上面這個文件中有不少綠色的註釋,帶有todo文字的,對於一個更復雜的系統,能夠用這些todo中的描述來設計獨立的模塊。
AggregatedResult.cs – 任務調度層服務的最終聚合結果定義
using CognitiveMiddlewareService.MiddlewareService; namespace CognitiveMiddlewareService.Processors { public class AggregatedResult { public LandmarkResult Landmark { get; set; } public CelebrityResult Celebrity { get; set; } } }
ValuesControllers.cs 注意Post的參數從[FromBody]變成了[FromForm],以便接收上傳的圖片流數據
using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using CognitiveMiddlewareService.CognitiveServices; using CognitiveMiddlewareService.Processors; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; namespace CognitiveMiddlewareService.Controllers { [Route("api/[controller]")] public class ValuesController : Controller { private readonly IProcessService processor; public ValuesController(IProcessService ps) { this.processor = ps; } // GET api/values [HttpGet] public IEnumerable<string> Get() { return new string[] { "value1", "value2" }; } // GET api/values/5 [HttpGet("{id}")] public string Get(int id) { return "value"; } // POST api/values [HttpPost] public async Task<string> Post([FromForm] IFormCollection formCollection) { try { IFormCollection form = await this.Request.ReadFormAsync(); IFormFile file = form.Files.First(); var bufferData = Helper.GetBuffer(file); var result = await this.processor.Process(bufferData); string jsonResult = JsonConvert.SerializeObject(result); // return json formatted data return jsonResult; } catch (Exception ex) { Debug.Write(ex.Message); return null; } } } }
Startup.cs
using CognitiveMiddlewareService.CognitiveServices; using CognitiveMiddlewareService.MiddlewareService; using CognitiveMiddlewareService.Processors; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace CognitiveMiddleService { public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddScoped<IProcessService, ProcessService>(); services.AddScoped<IVisionService, VisionService>(); services.AddScoped<ILandmarkService, LandmarkService>(); services.AddScoped<ICelebrityService, CelebrityService>(); services.AddScoped<IEntitySearchService, EntitySearchService>(); services.AddHttpClient(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseMvc(); } } }
除了第一行的services.AddMvc()之外,後面全部的行都是咱們須要增長的依賴注入代碼。
總結一下,從調用關係上看,是這個次序:
Controller -> ProcessService -> LandmarkService/CelebrityService -> VisionService/EntitySearchService
其中:
· Controller是個Endpoint
· ProcessService負責任務調度
· LandmarkService/CelebrityService是個集成服務,封裝了串行調用底層服務的邏輯
· VisionService/EntitySearchService是基礎服務,至關於最底層的原子操做
從數據結構上看,進化的順序是這樣的:
VisionResult/EntityResult -> CelebrityResult/LandmarkResult -> AggregatedResult
其中:
· VisionResult/EntityResult是最底層返回的原始結果,主要用於反序列化
· CelebrityResult/LandmarkResult是集成了多個原始結果後的抽象結果,好處是隔離了原始結果中的一些噪音,解耦,只返回咱們須要的字段
· AggregatedResult是聚合在一塊兒的結果,主要用於排序和生成返回JSON數據
有的人會問了:有必要搞這麼複雜嗎?這幾個調用在一個幫助函數裏不就能夠搞定了嗎?
確實是這樣,若是不考慮應用擴展什麼的,那就用一個幫助函數搞定;若是想玩兒點大的,那麼下面這張圖就是一個完整系統的Stack圖,這個系統經過組合調用多種微軟認知服務/微軟地圖服務/微軟實體服務等,可以提供給用戶的智能設備豐富的視覺對象識別體驗。
上圖包含了如下層次:
· Endpoints
Ø 兩個Endpoint,一個處理圖片輸入,另外一個處理文本輸入
· Processing and Classifier
Ø 包含圖像/文字的預處理/預分類
· Task Dispatcher
Ø 並行調用多種服務並協調同步關係
· API agent and Recognizer
Ø 組合調用各類API,內置的識別器(好比正則表達式)
· APIs
Ø 各類認知服務API
· Processors
Ø 隔離層/聚合層/排序器的組合稱呼
· Adaptive Card Generator
Ø 生成微軟最新推出的Adaptive Card技術的數據,供跨平臺客戶端接收並渲染
· Assistant Component
Ø 其它輔助組件
作好了一箇中間層服務,不是說簡單地向Azure上一部署就算完事兒了。任何一個商用的軟件,都須要嚴格的測試,對於普通的手機/客戶端軟件的測試,相信不少人都知道,覆蓋功能點,各類條件輸入,等等等等。對於中間層服務,除了功能點外,性能方面的測試尤爲重要。
如何進行測試呢?工欲善其事必先利其器,先看工具:
ASP.NET Core Web API有一套測試工具,請看這個連接:https://docs.microsoft.com/en-us/aspnet/core/test/?view=aspnetcore-2.1,它講述了一些列的方法,咱們再也不贅述,本文所要描述的是三種面向場景的測試方法:負載(較重的壓力)測試,(較輕的壓力)性能測試,(中等的壓力)穩定性測試。不是以show code爲主,而是以講理念爲主,懂得了理念,code容易寫啦。
對於一個普通的App,咱們用界面交互的方式進行測試。對於一個service,它的界面就至關於REST API,咱們能夠從客戶端發起測試,自動化程度較高。
在Visual Studio 2017,有專門的Load Test工具能夠幫助咱們完成在客戶端編寫測試代碼,調整各類測試參數,而後發起測試,具體的連接在這裏。
有了工具,再看方法和理念:
在本文中,咱們主要從概念上講解一下針對含有認知服務的中間服務層的測試方法,由於認知服務自己若是訪問量大的話,是要收取費用的!
小提示:各個認知服務的費用標準不一樣,請仔細閱讀相關網頁,以避免在進行大量的測試時引發沒必要要的費用發生。
模擬多個併發用戶訪問中間層服務,集中發生在一個持續的時間段內,以衡量服務質量。負載測試不斷的發展下去,負載愈來愈大,就會變成極限測試,最終把機器跑癱爲止。這種測試能夠幫助開發者知道在單機環境下能支持多少用戶,進而決定在Azure上要申請多少機器。
注意!咱們不是在測試認知服務的性能,是要測試本身的中間層服務的性能,因此以下圖所示:
要把認知服務用一個模擬的mock up service來代替,這個mock up service能夠本身簡單地用ASP.NET搭建一個,接收請求後,不作任何邏輯處理,直接返回JSON字符串,可是中間須要模擬認知服務的處理時間,故意延遲2~3秒。
另一個緣由是,認知服務比較複雜,可能不能知足很高的QPS的要求,而用本身的模擬服務能夠到達極高的QPS,這樣就不會正在測試中產生瓶頸。
網絡環境爲局域網內部,亦即客戶端、中間層、模擬服務都在局域網內部便可,這樣能夠避免網絡延遲帶來的干擾。
在本例中,咱們測試了8輪,每輪都模擬不一樣的併發用戶數持續運行一小時,最終結果以下:
concurrent users |
Idle |
1 user |
3 users |
5 users |
10 users |
25 users |
50 users |
75 users |
100 users |
CPU |
0% |
<1% |
<1% |
1% |
2.5% |
6% |
12% |
17% |
21% |
Memory(MB) |
110 |
116 |
150 |
158 |
164 |
176 |
260 |
301 |
335 |
Latency(s) |
0 |
2.61 |
2.61 |
2.61 |
2.62 |
2.63 |
2.64 |
2.67 |
2.7 |
Total Req. |
0 |
1,377 |
4,124 |
6,885 |
13,666 |
34,221 |
67,976 |
100,948 |
132,894 |
Failed Req. |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
QPS |
0.00 |
0.38 |
1.15 |
1.91 |
3.80 |
9.51 |
18.88 |
28.04 |
36.92 |
從圖表能夠看出,CPU/Memory/QPS都是線性增加的,意味着是能夠預測的。延遲(Latency)是平緩的,不會由於併發用戶變多而變慢,很健康。
在一個足夠長的時間內持續測試服務,中等負載,以檢查其可靠性。"足夠長"通常定義爲12小時、48小時、72小時等等。能夠認爲,被測對象只要跑夠了預約的時長,就算是穩定性過關了。
同理,咱們要測試的是中間層服務,而不是認知服務。測試環境與上面相同,也是使用模擬的認知服務,由於72小時的測試時間,會發送大量的請求,極可能超出了當月限額而收取費用。
網絡環境仍然使用局域網。
模擬10個併發用戶,持續向中間層服務發請求12小時,測試結果以下表:
Sample point |
CPU |
Memory |
Latency |
Total Request |
Failed |
QPS |
1:00:00 |
2.5% |
140M |
2.63 second |
13,730 |
0 |
3.81 |
2:00:00 |
2.5% |
160M |
2.61 second |
13,741 |
0 |
3.82 |
3:00:00 |
2.5% |
150M |
2.62 second |
13,728 |
0 |
3.81 |
…... |
|
|
|
|
|
|
Total/Average |
2.5% |
150M |
2.62 |
164,772 |
0 |
3.81 |
從CPU/Memory/Latency/QPS上來看,在12個小時內,都保持得很是穩定,說明服務器不會由於長時間運行而變得不穩定。
測試端對端(e2e)的請求/響應時間。這是針對某個服務場景的測試,想獲得具體的數值,因此不須要很大的負載壓力。
此次咱們須要使用真實的認知服務,網絡環境也使用真實的互聯網環境。亦即須要把中間服務層部署到互聯網上後進行測試,由於用模擬環境和局域網測試出來的數據不能表明實際的用戶使用狀況。
模擬1個用戶,持續向中間服務層發送請求1小時。而後模擬3個併發用戶,持續向中間服務層發送請求10分鐘。這兩種方法都不會對認知服務帶來很大的壓力。
在獲得了一系列的數據之後,每組數據都會有響應時間,咱們把它們按照從長(慢)到短(快)的順序排列,獲得下圖(其中橫座標是用戶數,縱座標是響應時間):
通常來講,咱們要考察幾個點,P90/P95/P99,好比P90的含義是:有90%的用戶的響應時間小於等於2449ms。這意味着若是有極個別用戶響應時間在10秒以上時,是一種正常的狀況;若是不少用戶(好比>5%)都在10秒以上就不正常了,須要馬上檢查服務器的運行狀態。
最後獲得的結果以下表,亦即性能指標:
Percentage |
P90 |
P95 |
P99 |
Average |
KPI |
<3000ms |
<3250 |
<4000 |
N/A |
Server-side processing time |
2449ms |
2652ms |
3571ms |
1675ms |
Test client e2e latency |
3160ms |
3368ms |
4369ms |
2317ms |
Server-side processing time: 服務器從接收到請求到發送回結果所花費的時間
Test client e2e latency: 客戶端從發送請求到接收響應所經歷的時間
在集成服務層增長能夠識別具備標準模式的文字的服務,好比電話號碼、網絡地址、郵件地址,這須要同時在基礎服務層增長OCR底層服務,並在任務調度層增長一個並行任務。
在本地測試好服務器的基本功能後,部署到Azure上去,看看代碼在實際環境中運行會有什麼問題。由於咱們不能實時地監控服務器,因此須要在服務層上增長log功能。
能夠選擇像Bob同窗那樣,先用第一種方式直接訪問微軟認知服務,而後一步步演進到中間層服務模式。建議使用VS2017 + Xamarin利器來實現跨平臺應用。
在任務調度層,增長一個本地的圖像分類器,如同"todo"裏的preprocess,可以把輸入圖片分類成"有人臉"、"有地標"、"有文字"等,而後再根據信心指數調用名人服務或地標服務,以減輕服務器的負擔,節省費用。好比,當"有地標"的信心指數小於0.5時,就終止後面的調用。這須要訓練一個圖片分類器,導出模型,再用Tools for AI作本地推理代碼。