今天要介紹的是微軟的語意分析服務 LUIS,製作聊天機器人最困難的部分是如何分析自然語言,也就是如何理解使用者想表達的意圖,而 LUIS 正是為了解決這個問題而誕生,微軟幫我們完成了語意分析底層的 AI,我們只需要根據需求建立範例語句,接著訓練、發布模型,就能透過呼叫服務的方式,得到 AI 分析的結果。
LUIS 的功能很多,建立訓練資料也需要一些經驗,我自己也沒有完全摸透,所以這篇只能帶大家簡單認識,更多的部分就給大家自行挖掘了。
LUIS 功能主要分為 意圖
和 實體
,意圖用於判斷使用者想做什麼事,例如: 我想買1杯珍珠奶茶、我想買1杯綠茶,這兩句話都是想買飲料,只是買的東西不同,買飲料
這件事就是一個意圖,建立意圖後就可以將這部分的邏輯做統一的處理。
而實體有點像變數,珍珠奶茶和綠茶可以看做飲料
實體,可以建立一個飲料實體並將所有的品項加入,程式內就可以像取變數一樣將飲料名取出,1杯、2杯也可以看做實體,數值類型的實體,實體的種類很多這裡就不一一介紹,後面會挑幾個比較常用的舉例說明。
接下來會以訂飲料為例子建立一個範例給大家看,首先進入 LUIS 管理頁面。
建立新應用程式,選擇要 LUIS 分析的語系。
點選生成
選單,建立訂飲料
意圖。
接著建立一些例句,例句越多預測的越準,這裡先將訂飲料簡單分為五個部分 品項
、甜度
、冰塊
、大小
、幾杯
,前三項為必填,後兩項為非必填。
例句:
實體可以分為下列幾大類:
Simple(簡單實體): 用於捕獲基本的文字
類型。
Composite(複合實體): 用於組合多個實體
,例如訂飲料需要品項實體、甜度實體、冰塊實體,可以將三者組合為一個飲料
複合實體描述飲料所包含的項目。
List(列表實體): 用於值是固定列表
或值具有同義詞
的場景,例如可以將所有的飲料品項列為清單實體,而同義詞則像珍奶為珍珠奶茶的同義詞、美金和美元也是。
Regex(正規式): 用於正規式。
還有一類特別的實體 預生成實體
,這類實體為系統內建,用於基本類型的資料,例如: 數字、日期、貨幣、網址、Email、等等...。
根據上面的介紹訂飲料範例需要建立下列實體:
品項: 飲料的品項有簡稱,例如珍奶和珍珠奶茶,建立為清單實體
。
甜度: 甜度為固定選項,建立為清單實體
。
冰塊: 冰塊為固定選項,建立為清單實體
。
大小: 大小為固定選項,建立為清單實體
。
幾杯: 幾杯為數字
+杯
,可以建立一個預生成實體 number
,加上一個列表實體單位
,單位可以是杯
或*
,再用複合使體將兩者結合表示幾杯
。
幾杯(複合實體) = 數字(number) + 單位(清單實體)
建立好的實體如下。
品項
甜度
冰塊
大小
單位
幾杯
接著回到意圖例句的畫面,設定例句中實體的部分。
設定完後點選上方的訓練
,LUIS 就會開始訓練 AI 模型。
接著可以開啟測試功能,看看目前例句捕獲到的實體是否精準。
LUIS 有匯入功能,這裡提供我的設定檔,方便讀者快速建立模型。
檔案需儲存為 .json
副檔名。
{
"luis_schema_version": "4.0.0",
"versionId": "0.1",
"name": "iBot",
"desc": "",
"culture": "zh-cn",
"tokenizerVersion": "1.0.0",
"intents": [
{
"name": "None"
},
{
"name": "訂飲料"
}
],
"entities": [],
"composites": [
{
"name": "幾杯",
"children": [
"number",
"單位"
],
"roles": []
}
],
"closedLists": [
{
"name": "冰塊",
"subLists": [
{
"canonicalForm": "正常冰",
"list": []
},
{
"canonicalForm": "少冰",
"list": []
},
{
"canonicalForm": "微冰",
"list": []
},
{
"canonicalForm": "去冰",
"list": []
}
],
"roles": []
},
{
"name": "品項",
"subLists": [
{
"canonicalForm": "茉莉綠茶",
"list": [
"綠茶"
]
},
{
"canonicalForm": "阿薩姆紅茶",
"list": [
"紅茶"
]
},
{
"canonicalForm": "四季春春青茶",
"list": [
"四季春"
]
},
{
"canonicalForm": "黃金烏龍",
"list": [
"烏龍"
]
},
{
"canonicalForm": "波霸紅茶",
"list": [
"波霸紅"
]
},
{
"canonicalForm": "梅子綠",
"list": []
},
{
"canonicalForm": "冰淇淋紅茶",
"list": []
},
{
"canonicalForm": "珍珠奶茶",
"list": [
"珍奶"
]
},
{
"canonicalForm": "波霸奶茶",
"list": []
},
{
"canonicalForm": "檸檬梅汁",
"list": []
},
{
"canonicalForm": "檸檬多多",
"list": []
},
{
"canonicalForm": "波霸紅茶拿鐵",
"list": []
},
{
"canonicalForm": "珍珠紅茶拿鐵",
"list": []
},
{
"canonicalForm": "可可芭蕾",
"list": []
}
],
"roles": []
},
{
"name": "單位",
"subLists": [
{
"canonicalForm": "杯",
"list": []
},
{
"canonicalForm": "*",
"list": []
}
],
"roles": []
},
{
"name": "大小",
"subLists": [
{
"canonicalForm": "大杯",
"list": [
"L",
"l",
"大"
]
},
{
"canonicalForm": "中杯",
"list": [
"M",
"m",
"中"
]
}
],
"roles": []
},
{
"name": "甜度",
"subLists": [
{
"canonicalForm": "少糖",
"list": []
},
{
"canonicalForm": "半糖",
"list": []
},
{
"canonicalForm": "微糖",
"list": []
},
{
"canonicalForm": "無糖",
"list": []
},
{
"canonicalForm": "正常甜",
"list": [
"正常糖"
]
}
],
"roles": []
}
],
"patternAnyEntities": [],
"regex_entities": [],
"prebuiltEntities": [
{
"name": "number",
"roles": []
}
],
"model_features": [],
"regex_features": [],
"patterns": [
{
"pattern": "{品項} {甜度} {冰塊} {單位}{number}",
"intent": "訂飲料"
}
],
"utterances": [
{
"text": "1杯冰淇淋紅茶少糖微冰L",
"intent": "訂飲料",
"entities": [
{
"entity": "幾杯",
"startPos": 0,
"endPos": 1
}
]
},
{
"text": "可可芭蕾無糖去冰*20",
"intent": "訂飲料",
"entities": [
{
"entity": "幾杯",
"startPos": 8,
"endPos": 10
}
]
},
{
"text": "梅子綠去冰無糖506杯",
"intent": "訂飲料",
"entities": [
{
"entity": "幾杯",
"startPos": 7,
"endPos": 10
}
]
},
{
"text": "檸檬多多正常甜正常冰中杯",
"intent": "訂飲料",
"entities": []
},
{
"text": "檸檬梅汁少冰無糖M 5杯",
"intent": "訂飲料",
"entities": [
{
"entity": "幾杯",
"startPos": 10,
"endPos": 11
}
]
},
{
"text": "波霸紅茶拿鐵微糖正常冰*1",
"intent": "訂飲料",
"entities": [
{
"entity": "幾杯",
"startPos": 11,
"endPos": 12
}
]
},
{
"text": "珍奶正常糖正常冰L *58",
"intent": "訂飲料",
"entities": [
{
"entity": "幾杯",
"startPos": 10,
"endPos": 12
}
]
},
{
"text": "珍珠奶茶微糖去冰",
"intent": "訂飲料",
"entities": []
},
{
"text": "紅茶 半糖 去冰",
"intent": "訂飲料",
"entities": []
},
{
"text": "綠茶半糖少冰",
"intent": "訂飲料",
"entities": []
}
],
"settings": []
}
測試完後點選上方選單的發布
,接著選擇生產
後按發布。
接下來幾個資訊必須記住程式中會用到 應用程式ID
、密鑰
、地區
。
需安裝的套件(Nuget):
在 appsettings.json
新增下列三個參數分別對應:
完整的 appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"LineBot": {
"channelSecret": "...",
"accessToken": "...",
"luisAppId": "...",
"luisAppKey": "...",
"luisDomain": "westus"
}
}
調整 LineBotConfig.cs
public class LineBotConfig
{
public string channelSecret { get; set; }
public string accessToken { get; set; }
public string luisAppId { get; set; }
public string luisAppKey { get; set; }
public string luisDomain { get; set; }
}
調整 Startup.cs
services.AddSingleton<LineBotConfig, LineBotConfig>((s) => new LineBotConfig
{
channelSecret = Configuration["LineBot:channelSecret"],
accessToken = Configuration["LineBot:accessToken"],
luisAppId = Configuration["LineBot:luisAppId"],
luisAppKey = Configuration["LineBot:luisAppKey"],
luisDomain = Configuration["LineBot:luisDomain"]
});
建立 LUISRuntimeClientEx.cs 並在內部新增 GetPrediction
方法方便程式呼叫。
public class LUISRuntimeClientEx: LUISRuntimeClient
{
private readonly string _appId;
public LUISRuntimeClientEx(ServiceClientCredentials credentials, string appId)
: base(credentials)
{
_appId = appId;
}
public async Task<LuisResult> GetPrediction(string query)
{
var prediction = new Prediction(this);
return await prediction.ResolveAsync(
appId: _appId,
query: query,
timezoneOffset: null,
verbose: true,
staging: false,
spellCheck: false,
bingSpellCheckSubscriptionKey: null,
log: false,
cancellationToken: CancellationToken.None);
}
}
LineBotController.cs 新增 LUISRuntimeClientEx
//luisClient
var luisClientEx = new LUISRuntimeClientEx(
new ApiKeyServiceClientCredentials(_lineBotConfig.luisAppKey), _lineBotConfig.luisAppId);
luisClientEx.Endpoint = $"https:/{_lineBotConfig.luisDomain.api.cognitive.microsoft.com";
完整的 LineBotController.cs
[Route("api/linebot")]
public class LineBotController : Controller
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly HttpContext _httpContext;
private readonly LineBotConfig _lineBotConfig;
private readonly ILogger _logger;
public LineBotController(IServiceProvider serviceProvider,
LineBotConfig lineBotConfig,
ILogger<LineBotController> logger)
{
_httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>();
_httpContext = _httpContextAccessor.HttpContext;
_lineBotConfig = lineBotConfig;
_logger = logger;
}
[HttpPost("run")]
public async Task<IActionResult> Post()
{
try
{
//lineMessagingClient
var events = await _httpContext.Request.GetWebhookEventsAsync(_lineBotConfig.channelSecret);
var lineMessagingClient = new LineMessagingClient(_lineBotConfig.accessToken);
//luisClient
var luisClientEx = new LUISRuntimeClientEx(
new ApiKeyServiceClientCredentials(_lineBotConfig.luisAppKey), _lineBotConfig.luisAppId);
luisClientEx.Endpoint = $"https://{_lineBotConfig.luisDomain}.api.cognitive.microsoft.com";
var lineBotApp = new LineBotApp(lineMessagingClient, luisClientEx);
await lineBotApp.RunAsync(events);
}
catch (Exception ex)
{
_logger.LogError(JsonConvert.SerializeObject(ex));
}
return Ok();
}
}
主要的程式邏輯會寫在 LineBotApp.cs,首先調整建構式如下
...
private readonly LUISRuntimeClientEx _LUISRuntimeClientEx;
public LineBotApp(LineMessagingClient lineMessagingClient,LUISRuntimeClientEx LUISRuntimeClientEx)
{
...
_LUISRuntimeClientEx = LUISRuntimeClientEx;
}
主要程式邏輯:
將使用者訊息丟給 LUIS 取得分析結果
判斷意圖是否為訂飲料
如果是就將實體取出分析
如實體有缺少則返回提示訊息
如實體資料正確,則整理並計算金額後回傳
1. 將使用者訊息丟給 LUIS 取得分析結果
呼叫剛剛新增的 GetPrediction 方法,可以取得 LUIS 分析的結果。
var luisResult = await _LUISRuntimeClientEx.GetPrediction(text);
回傳格式如下:
以 "2杯綠茶半糖去冰m" 為例
{
"query": "2杯綠茶半糖去冰m",
"alteredQuery": null,
"topScoringIntent": {
"intent": "訂飲料",
"score": 0.989621758
},
"intents": [
{
"intent": "訂飲料",
"score": 0.989621758
},
{
"intent": "None",
"score": 0.0114329439
}
],
"entities": [
{
"entity": "2 杯",
"type": "幾杯",
"startIndex": 0,
"endIndex": 1,
"score": 0.51323694
},
{
"entity": "2",
"type": "builtin.number",
"startIndex": 0,
"endIndex": 0,
"resolution": {
"value": "2"
}
},
{
"entity": "半",
"type": "builtin.number",
"startIndex": 4,
"endIndex": 4,
"resolution": {
"value": "0.5"
}
},
{
"entity": "去冰",
"type": "冰塊",
"startIndex": 6,
"endIndex": 7,
"resolution": {
"values": [
"去冰"
]
}
},
{
"entity": "綠茶",
"type": "品項",
"startIndex": 2,
"endIndex": 3,
"resolution": {
"values": [
"茉莉綠茶"
]
}
},
{
"entity": "杯",
"type": "單位",
"startIndex": 1,
"endIndex": 1,
"resolution": {
"values": [
"杯"
]
}
},
{
"entity": "m",
"type": "大小",
"startIndex": 8,
"endIndex": 8,
"resolution": {
"values": [
"中杯"
]
}
},
{
"entity": "半糖",
"type": "甜度",
"startIndex": 4,
"endIndex": 5,
"resolution": {
"values": [
"半糖"
]
}
}
],
"compositeEntities": [
{
"parentType": "幾杯",
"value": "2 杯",
"children": [
{
"type": "builtin.number",
"value": "2"
},
{
"type": "單位",
"value": "杯"
}
]
}
],
"sentimentAnalysis": null,
"connectedServiceResult": null
}
2. 判斷意圖是否為訂飲料
LUIS 有一個預設的意圖 None
,當分析的結果都不符合我們建立的意圖時,會歸類到 None,另外 LUIS 還會給結果打 分數
,這邊建議將分數設個限制,否則會常常誤判,例如我輸入 "太好笑了" 也被歸類到訂飲料上 ~"~
if (luisResult.TopScoringIntent.Intent == "None" ||
luisResult.TopScoringIntent.Score < 0.8)
{
result.Add(new TextMessage("無法理解"));
return result;
}
3. 如果是就將實體取出分析
這邊介紹幾種取得實體的方式
//取得甜度
var sugar = luisResult.Entities.FirstOrDefault(it => it.Type == "甜度");
//取得幾杯複合實體
var countCompositeEntity = luisResult.CompositeEntities.FirstOrDefault(it => it.ParentType == "幾杯");
//取得幾杯
var count = countCompositeEntity?.Children.FirstOrDefaul(it => it.Type == "builtin.number");
列表實體如果有設定別名,則會在 JSON 內附帶規範化文字
,可以看到上方的綠茶實體,resolution
屬性內的 茉莉綠茶
就是規範化文字,這個設計可以方便程式將相同的東西做統一的處理,寫法蠻特別的,如下:
//取得甜度規範化文字
var sugar = luisResult.Entities.FirstOrDefault(it => it.Type == "甜度");
sugar.AdditionalProperties.TryGetValue("resolution", out dynamic sugarResolution);
var sugarValue = sugarResolution.values[0];
4. 如實體有缺少則返回提示訊息
完整部分可參考下方完整程式。
...
if (drink == null)
{
result.Add(new TextMessage($"缺少或查無飲料品項"));
validate = false;
}
...
5. 如實體資料正確,則整理並計算金額後回傳
完整部分可參考下方完整程式。
if (validate)
{
...
//計算金額
var price = _drinkPrice[$"{drinkValue}{sizeValue}"] * int.Parse(countValue);
//回傳結果
result.Add(new TextMessage($"{drinkValue} {sugarValue} {iceValue} {sizeValue} {countValue}杯 金額 {price}元"));
}
如何將 luisResult 轉成 JSON
題外話,紀錄一下如何將 luisResult 轉成 JSON。
//原始JSON資料
result.Add(new TextMessage($"{JsonConvert.SerializeObjec(luisResult)}"));
完整的 LineBotApp.cs
public class LineBotApp : WebhookApplication
{
private readonly LineMessagingClient _messagingClient;
private readonly LUISRuntimeClientEx _LUISRuntimeClientEx;
public LineBotApp(LineMessagingClient lineMessagingClient, LUISRuntimeClientEx LUISRuntimeClientEx)
{
_messagingClient = lineMessagingClient;
_LUISRuntimeClientEx = LUISRuntimeClientEx;
}
protected override async Task OnMessageAsync(MessageEvent ev)
{
var result = null as List<ISendMessage>;
switch (ev.Message)
{
//文字訊息
case TextEventMessage textMessage:
{
//頻道Id
var channelId = ev.Source.Id;
//使用者Id
var userId = ev.Source.UserId;
//LUIS
result = await LUIS(channelId, textMessage.Text);
if (result != null)
break;
}
break;
}
if (result != null)
await _messagingClient.ReplyMessageAsync(ev.ReplyToken, result);
}
//定義飲料對應的價格
private static readonly Dictionary<string, int> _drinkPrice = new Dictionary<string, int>
{
["茉莉綠茶中杯"] = 25,
["茉莉綠茶大杯"] = 30,
["阿薩姆紅茶中杯"] = 25,
["阿薩姆紅茶大杯"] = 30,
["四季春春青茶中杯"] = 25,
["四季春春青茶大杯"] = 30,
["黃金烏龍中杯"] = 25,
["黃金烏龍大杯"] = 30,
["波霸紅茶中杯"] = 30,
["波霸紅茶大杯"] = 40,
["梅子綠中杯"] = 35,
["梅子綠大杯"] = 50,
["冰淇淋紅茶中杯"] = 35,
["冰淇淋紅茶大杯"] = 50,
["珍珠奶茶中杯"] = 35,
["珍珠奶茶大杯"] = 50,
["波霸奶茶中杯"] = 35,
["波霸奶茶大杯"] = 50,
["檸檬梅汁中杯"] = 45,
["檸檬梅汁大杯"] = 60,
["檸檬多多中杯"] = 50,
["檸檬多多大杯"] = 70,
["波霸紅茶拿鐵中杯"] = 45,
["波霸紅茶拿鐵大杯"] = 60,
["珍珠紅茶拿鐵中杯"] = 45,
["珍珠紅茶拿鐵大杯"] = 60,
["可可芭蕾中杯"] = 45,
["可可芭蕾大杯"] = 60,
};
protected async Task<List<ISendMessage>> LUIS(string channelId, string text)
{
var luisResult = await _LUISRuntimeClientEx.GetPrediction(text);
var result = new List<ISendMessage>();
if (luisResult.TopScoringIntent.Intent == "None" ||
luisResult.TopScoringIntent.Score < 0.8)
{
result.Add(new TextMessage("無法理解"));
return result;
}
//取得品項
var drink = luisResult.Entities.FirstOrDefault(it => it.Type == "品項");
//取得甜度
var sugar = luisResult.Entities.FirstOrDefault(it => it.Type == "甜度");
//取得冰塊
var ice = luisResult.Entities.FirstOrDefault(it => it.Type == "冰塊");
//取得大小
var size = luisResult.Entities.FirstOrDefault(it => it.Type == "大小");
//取得幾杯複合實體
var countCompositeEntity = luisResult.CompositeEntities?.FirstOrDefault(it => it.ParentType == "幾杯");
//取得幾杯
var count = countCompositeEntity?.Children.FirstOrDefault(it => it.Type == "builtin.number");
//顯示意圖和分數
result.Add(new TextMessage($"意圖: {luisResult.TopScoringIntent.Intent}({luisResult.TopScoringIntent.Score?.ToString("0.####") ?? "0"})"));
//檢查必填欄位是否都有值
var validate = true;
if (drink == null)
{
result.Add(new TextMessage($"缺少或查無飲料品項"));
validate = false;
}
else if (sugar == null)
{
result.Add(new TextMessage($"缺少甜度,範例: 正常甜、少糖、半糖、微糖、無糖"));
validate = false;
}
else if (ice == null)
{
result.Add(new TextMessage($"缺少冰塊,範例: 正常冰、少冰、微冰、去冰"));
validate = false;
}
//資料正確
if (validate)
{
//取得品項規範化文字
drink.AdditionalProperties.TryGetValue("resolution", out dynamic drinkResolution);
var drinkValue = drinkResolution.values[0];
//取得甜度規範化文字
sugar.AdditionalProperties.TryGetValue("resolution", out dynamic sugarResolution);
var sugarValue = sugarResolution.values[0];
//取得冰塊文字
var iceValue = ice.Entity;
//取得大小規範化文字,預設為大杯
dynamic sizeResolution = null;
size?.AdditionalProperties.TryGetValue("resolution", out sizeResolution);
var sizeValue = size == null ? "大杯" : sizeResolution.values[0];
//取得幾杯
var countValue = count == null ? "1" : count.Value;
//計算金額
var price = _drinkPrice[$"{drinkValue}{sizeValue}"] * int.Parse(countValue);
//回傳結果
result.Add(new TextMessage($"{drinkValue} {sugarValue} {iceValue} {sizeValue} {countValue}杯 金額 {price}元"));
}
//原始JSON資料
//result.Add(new TextMessage($"{JsonConvert.SerializeObject(luisResult)}"));
return result;
}
}
這篇簡單的介紹了 LUIS 最核心的兩個功能 意圖
和 實體
,用訂飲料當作範例,舉了幾個實體應用的場景,希望讓大家更了解不同實體的使用時機,當然我的用法可能也不完全正確,小弟也是第一次實作,如有錯誤再麻煩各位大大指正。
LUIS 改版還蠻快的,自己研究的時候還在 V2,現在已經更新到 V3 了,也還有很多功能是我沒深入研究的,可以玩的地方還很多,留給有興趣的大大自行挖掘,下一篇會介紹如何建立 Line Bot 的圖文選單,今天就到這裡,感謝大家觀看。
持續補坑中,鐵人賽發文功能不要太快關閉啊啊啊~~~