提交按鈕用來將輸入的信息提交到服務器。代碼格式如下:
<input type="submit" name="..." value="...">
其中type="submit"定義提交按鈕;name屬性定義提交按鈕的名稱;value屬性定義按鈕的顯示文字。通過提交按鈕,可以將表單的信息提交給表單里action所指向的文件。
(1)編寫代碼如下圖所示,在<body>標簽中加入以下代碼。
(2)在瀏覽器中打開文件,預覽效果圖如下所示,輸入內容按【提交】按鈕,即可將表單中的數(shù)據(jù)發(fā)送到指定的文件。
復位按鈕用來重置表單中輸入的信息。代碼格式如下:
<input type="reset" name="..." value="..." >
其中type="reset"定義復位按鈕;name屬性定義復位按鈕的名稱;value屬性定義按鈕的顯示文字。
(1)編寫代碼如下圖所示,在<body>標簽中加入以下代碼。
(2)在瀏覽器中打開文件,預覽效果圖如下所示,輸入內容后單機【重置】按鈕,即可將表單中的數(shù)據(jù)清空。
者 | 無名之輩FTER
責編 | 夕顏
出品 | 程序人生(ID:coder_life)
本文翻譯自Rasa官方文檔,并融合了自己的理解和項目實戰(zhàn),同時對文檔中涉及到的技術點進行了一定程度的擴展,目的是為了更好的理解Rasa工作機制。與本文配套的項目GitHub地址:ChitChatAssistant https://github.com/jiangdongguo/ChitChatAssistant,歡迎star和issues,我們共同討論、學習!
對話管理
1.1 多輪對話
多輪對話是相對于單輪對話而言的,單輪對話側重于一問一答,即直接根據(jù)用戶的問題給出精準的答案。問答更接近一個信息檢索的過程,雖然也可能涉及簡單的上下文處理,但通常是通過指代消解和 query 補全來完成的,而多輪對話側重于需要維護一個用戶目標狀態(tài)的表示和一個決策過程來完成任務,具體來說就是用戶帶著明確的目的而來,希望得到滿足特定限制條件的信息或服務,例如:訂餐,訂票,尋找音樂、電影或某種商品等。因為用戶的需求可以比較復雜,可能需要分多輪進行陳述,用戶也可能在對話過程中不斷修改或完善自己的需求。此外,當用戶的陳述的需求不夠具體或明確的時候,機器也可以通過詢問、澄清或確認來幫助用戶找到滿意的結果。
因此,任務驅動的多輪對話不是一個簡單的自然語言理解加信息檢索的過程,而是一個決策過程,需要機器在對話過程中不斷根據(jù)當前的狀態(tài)決策下一步應該采取的最優(yōu)動作(如:提供結果,詢問特定限制條件,澄清或確認需求,等等)從而最有效的輔助用戶完成信息或服務獲取的任務。
注:任務驅動的多輪對話系統(tǒng)通常為封閉域(domain)(閑聊系統(tǒng)為開放域),而特定限制條件對應于槽(Slot),也就是說,用戶在滿足特定限制條件時就是一次槽值填充的過程,如果用戶能夠在一次會話中,滿足全部的限制條件,那么就不必進行多輪對話,即可直接使用戶得到滿意的信息或服務。
1.2 對話管理
對話管理,即Dialog Management(DM),它控制著人機對話的過程,是人機對話系統(tǒng)的重要組成部分。DM會根據(jù)NLU模塊輸出的語義表示執(zhí)行對話狀態(tài)的更新和追蹤,并根據(jù)一定策略選擇相應的候選動作。簡單來說,就是DM會根據(jù)對話歷史信息,決定此刻對用戶的反應,比如在任務驅動的多輪對話系統(tǒng)中,用戶帶著明確的目的如訂餐、訂票等,用戶需求比較復雜,有很多限制條件,可能需要分多輪進行陳述,一方面,用戶在對話過程中可以不斷修改或完善自己的需求,另一方面,當用戶的陳述的需求不夠具體或明確的時候,機器也可以通過詢問、澄清或確認來幫助用戶找到滿意的結果。如下圖所示,DM 的輸入就是用戶輸入的語義表達(或者說是用戶行為,是 NLU 的輸出)和當前對話狀態(tài),輸出就是下一步的系統(tǒng)行為和更新的對話狀態(tài)。這是一個循環(huán)往復不斷流轉直至完成任務的過程。
從本質上來說,**任務驅動的對話管理實際就是一個決策過程,系統(tǒng)在對話過程中不斷根據(jù)當前狀態(tài)決定下一步應該采取的最優(yōu)動作(如:提供結果,詢問特定限制條件,澄清或確認需求等),從而最有效的輔助用戶完成信息或服務獲取的任務。**對話管理的任務大致有:
對話狀態(tài)維護(dialog state tracking, DST)
對話狀態(tài)是指記錄了哪些槽位已經被填充、下一步該做什么、填充什么槽位,還是進行何種操作。用數(shù)學形式表達為,t+1 時刻的對話狀態(tài)S(t+1),依賴于之前時刻 t 的狀態(tài)St,和之前時刻 t 的系統(tǒng)行為At,以及當前時刻 t+1 對應的用戶行為O(t+1)。可以寫成S(t+1)←St+At+O(t+1)。
生成系統(tǒng)決策(dialog policy)
根據(jù) DST 中的對話狀態(tài)(DS),產生系統(tǒng)行為(dialog act),決定下一步做什么 dialog act 可以表示觀測到的用戶輸入(用戶輸入 -> DA,就是 NLU 的過程),以及系統(tǒng)的反饋行為(DA -> 系統(tǒng)反饋,就是 NLG 的過程)。
作為接口與后端/任務模型進行交互
Rasa Core
Rasa Core是Rasa框架提供的對話管理模塊,它類似于聊天機器人的大腦,主要的任務是維護更新對話狀態(tài)和動作選擇,然后對用戶的輸入作出響應。所謂對話狀態(tài)是一種機器能夠處理的對聊天數(shù)據(jù)的表征,對話狀態(tài)中包含所有可能會影響下一步決策的信息,如自然語言理解模塊的輸出、用戶的特征等;所謂動作選擇,是指基于當前的對話狀態(tài),選擇接下來合適的動作,例如向用戶追問需補充的信息、執(zhí)行用戶要求的動作等。舉一個具體的例子,用戶說“幫我媽媽預定一束花”,此時對話狀態(tài)包括自然語言理解模塊的輸出、用戶的位置、歷史行為等特征。在這個狀態(tài)下,系統(tǒng)接下來的動作可能是:
向用戶詢問可接受的價格,如“請問預期價位是多少?”;
向用戶確認可接受的價格,如“像上次一樣買價值200的花可以嗎?”
直接為用戶預訂
2.1 Stories
Rasa的故事是一種訓練數(shù)據(jù)的形式,用來訓練Rasa的對話管理模型。故事是用戶和人工智能助手之間的對話的表示,轉換為特定的格式,其中用戶輸入表示為相應的意圖(和必要的實體),而助手的響應表示為相應的操作名稱。Rasa核心對話系統(tǒng)的一個訓練示例稱為一個故事。這是一個故事數(shù)據(jù)格式的指南。兩段對話樣本示例:
<!-- ##表示story的描述,沒有實際作用 -->
## greet + location/price + cuisine + num people
* greet
- utter_greet
* inform{"location": "rome", "price": "cheap"}
- action_on_it
- action_ask_cuisine
* inform{"cuisine": "spanish"}
- action_ask_numpeople
* inform{"people": "six"}
- action_ack_dosearch
<!-- Form Action-->
## happy path
* request_weather
- weather_form
- form{"name": "weather_form"}
- form{"name": }
Story格式大致包含三個部分:
1. 用戶輸入(User Messages)
使用*開頭的語句表示用戶的輸入消息,我們無需使用包含某個具體內容的輸入,而是使用NLU管道輸出的intent和entities來表示可能的輸入。需要注意的是,如果用戶的輸入可能包含entities,建議將其包括在內,將有助于policies預測下一步action。這部分大致包含三種形式,示例如下:
(1)* greet 表示用戶輸入沒有entity情況;
(2)* inform{"people": "six"} 表示用戶輸入包含entity情況,響應這類intent為普通action;
(3)* request_weather 表示用戶輸入Message對應的intent為form action情況;
2. 動作(Actions)
使用-開頭的語句表示要執(zhí)行動作(Action),可分為utterance actions和custom actions,其中,前者在domain.yaml中定義以utter_為前綴,比如名為greet的意圖,它的回復應為utter_greet;后者為自定義動作,具體邏輯由我們自己實現(xiàn),雖然在定義action名稱的時候沒有限制,但是還是建議以action_為前綴,比如名為inform的意圖fetch_profile的意圖,它的response可為action_fetch_profile。
3. 事件(Events)
Events也是使用-開頭,主要包含槽值設置(SlotSet)和激活/注銷表單(Form),它是是Story的一部分,并且必須顯示的寫出來。Slot Events和Form Events的作用如下:
(1)Slot Events
Slot Events的作用當我們在自定義Action中設置了某個槽值,那么我們就需要在Story中Action執(zhí)行之后顯著的將這個SlotSet事件標注出來,格式為- slot{"slot_name": "value"}。比如,我們在action_fetch_profile中設置了Slot名為account_type的值,代碼如下:
from rasa_sdk.actions import Action
from rasa_sdk.events import SlotSet
import requests
class FetchProfileAction(Action):
def name(self):
return "fetch_profile"
def run(self, dispatcher, tracker, domain):
url = "http://myprofileurl.com"
data = requests.get(url).json
return [SlotSet("account_type", data["account_type"])]
那么,就需要在Story中執(zhí)行action_fetch_profile之后,添加- slot{"account_type" : "premium"}。雖然,這么做看起來有點多余,但是Rasa規(guī)定這么做必須的,目的是提高訓練時準確度。
## fetch_profile
* fetch_profile
- action_fetch_profile
- slot{"account_type" : "premium"}
- utter_welcome_premium
當然,如果您的自定義Action中將槽值重置為None,則對應的事件為-slot{"slot_name": }。
(2)Form Events
在Story中主要存在三種形式的表單事件(Form Events),它們可表述為:
Form Action事件
Form Action即表單動作事件,是自定義Action的一種,用于一個表單操作。示例如下:
- restaurant_form
Form activation事件
form activation即激活表單事件,當form action事件執(zhí)行后,會立馬執(zhí)行該事件。示例如下:
- form{"name": "restaurant_form"}
Form deactivation事件
form deactivation即注銷表單事件,作用與form activation相反。示例如下:
- form{"name": }
總之,我們在構建Story時,可以說是多種多樣的,因為設計的故事情節(jié)是多種多樣的,這就意味著上述三種內容的組合也是非常靈活的。另外,在設計Story時Rasa還提供了Checkpoints 和OR statements兩種功能,來提升構建Story的靈活度,但是需要注意的是,東西雖好,但是不要太貪了,過多的使用不僅增加了復雜度,同時也會拖慢訓練的速度。其中,Checkpoints用于模塊化和簡化訓練數(shù)據(jù),示例如下:
## first story
* greet
- action_ask_user_question
> check_asked_question
## user affirms question
> check_asked_question
* affirm
- action_handle_affirmation
> check_handled_affirmation
## user denies question
> check_asked_question
* deny
- action_handle_denial
> check_handled_denial
## user leaves
> check_handled_denial
> check_handled_affirmation
* goodbye
- utter_goodbye
在上面的例子中,可以使用> check_asked_question表示first story,這樣在其他story中,如果有相同的first story部分,可以直接用> check_asked_question代替。而OR Statements主要用于實現(xiàn)某一個action可同時響應多個意圖的情況,比如下面的例子:
## story
* affirm OR thankyou
- action_handle_affirmation
2.2 Domain
Domain,譯為**“領域”**,它描述了對話機器人應知道的所有信息,類似于“人的大腦”,存儲了意圖intents、實體entities、插槽slots以及動作actions等信息,其中,intents、entities在NLU訓練樣本中定義,slots對應于entities類型,只是表現(xiàn)形式不同。domain.yml文件組成結構如下:
具體介紹如下:
1. intents
intents:
- affirm
- deny
- greet
- request_weather
- request_number
- inform
- inform_business
- stop
- chitchat
intents,即意圖,是指我們輸入一段文本,希望Bot能夠明白這段文本是什么意思。在Rasa框架中,意圖的定義是在NLU樣本中實現(xiàn)的,并且在每個意圖下面我們需要枚舉盡可多的樣本用于訓練,以達到Bot能夠準確識別出我們輸入的一句話到底想要干什么。
2. session_config
session_config:
carry_over_slots_to_new_session: true
session_expiration_time: 60
session_config,即會話配置,這部分的作用為配置一次會話(conversation session)是否有超時限制。上例演示的是,每次會話的超時時間為60s,如果用戶開始一段會話后,在60s內沒有輸入任何信息,那么這次會話將被結束,然后Bot又會開啟一次新的會話,并將上一次會話的Slot值拷貝過來。當然,我們希望舍棄上一次會話Slot的值,可以將carry_over_slots_to_new_session設置為false。另外,當session_expiration_time被設置為0時,Bot永遠不會結束當前會話并一直等待用戶輸入(注:執(zhí)行action_session_start仍然可以開始一次新的會話,在設置為0的情況下)。
3. slots
slots:
date_time:
type: unfeaturized
auto_fill: false
address:
type: unfeaturized
auto_fill: false
Slots,即插槽,它就像對話機器人的內存,它通過鍵值對的形式可用來收集存儲用戶輸入的信息(實體)或者查詢數(shù)據(jù)庫的數(shù)據(jù)等。關于Slots的設計與使用,詳情請見本文2.6小節(jié)。
4. entities
entities:
- date_time
- address
entities,即實體,類似于輸入文本中的關鍵字,需要在NLU樣本中進行標注,然后Bot進行實體識別,并將其填充到Slot槽中,便于后續(xù)進行相關的業(yè)務操作。
5. actions
actions:
- utter_answer_affirm # utter_開頭的均為utter actions
- utter_answer_deny
- utter_answer_greet
- utter_answer_goodbye
- utter_answer_thanks
- utter_answer_whoareyou
- utter_answer_whattodo
- utter_ask_date_time
- utter_ask_address
- utter_ask_number
- utter_ask_business
- utter_ask_type
- action_default_fallback # default actions
當Rasa NLU識別到用戶輸入Message的意圖后,Rasa Core對話管理模塊就會對其作出回應,而完成這個回應的模塊就是action。Rasa Core支持三種action,即default actions、utter actions以及 custom actions。關于如何實現(xiàn)Actions和處理業(yè)務邏輯,我們在一篇文章中詳談,這里僅作簡單了解。
6. forms
forms:
- weather_form
forms,即表單,該部分列舉了在NLU樣本中定義了哪些Form Actions。關于Form Actions的相關知識,請移步至本文的2.7小節(jié)。
7. responses
responses:
utter_answer_greet:
- text: "您好!請問我可以幫到您嗎?"
- text: "您好!很高興為您服務。請說出您要查詢的功能?"
utter_ask_date_time:
- text: "請問您要查詢哪一天的天氣?"
utter_ask_address:
- text: "請問您要查下哪里的天氣?"
utter_default:
- text: "沒聽懂,請換種說法吧~"
responses部分就是描述UtterActions具體的回復內容,并且每個UtterAction下可以定義多條信息,當用戶發(fā)起一個意圖,比如 “你好!”,就觸發(fā)utter_answer_greet操作,Rasa Core會從該action的模板中自動選擇其中的一條信息作為結果反饋給用戶。
2.3 Responses
Responses的作用就是自動響應用戶輸入的信息,因此我們需要管理這些響應(Responses)。Rasa框架提供了三種方式來管理Responses,它們是:
在domain.yaml文件中存儲Responses;
在訓練數(shù)據(jù)中存儲Responses;
自定義一個NLG服務來生成Responses。
由于第一種我們在本文2.2(7)小節(jié)有過介紹,而創(chuàng)建NLG服務是這樣的:
nlg:
url: http://localhost:5055/nlg # url of the nlg endpoint
# you can also specify additional parameters, if you need them:
# headers:
# my-custom-header: value
# token: "my_authentication_token" # will be passed as a get parameter
# basic_auth:
# username: user
# password: pass
# example of redis external tracker store config
tracker_store:
type: redis
url: localhost
port: 6379
db: 0
password: password
record_exp: 30000
# example of mongoDB external tracker store config
#tracker_store:
#type: mongod
#url: mongodb://localhost:27017
#db: rasa
#user: username
#password: password
2.4 Actions
當Rasa NLU識別到用戶輸入Message的意圖后,Rasa Core對話管理模塊就會對其作出回應,而完成這個回應的模塊就是action。Rasa Core支持三種action,即default actions、utter actions以及 custom actions。關于如何實現(xiàn)Actions和處理業(yè)務邏輯,我們在一篇文章中詳談,這里僅作簡單了解。
1. default actions
DefaultAction是Rasa Core默認的一組actions,我們無需定義它們,直接可以story和domain中使用。包括以下三種action:
action_listen:監(jiān)聽action,Rasa Core在會話過程中通常會自動調用該action;
action_restart:重置狀態(tài),比初始化Slots(插槽)的值等;
action_default_fallback:當Rasa Core得到的置信度低于設置的閾值時,默認執(zhí)行該action;
2. utter actions
UtterAction是以utter_為開頭,僅僅用于向用戶發(fā)送一條消息作為反饋的一類actions。定義一個UtterAction很簡單,只需要在domain.yml文件中的actions:字段定義以utter_為開頭的action即可,而具體回復內容將被定義在templates:部分,這個我們下面有專門講解。定義utter actions示例如下:
actions:
- utter_answer_greet
- utter_answer_goodbye
- utter_answer_thanks
- utter_introduce_self
- utter_introduce_selfcando
- utter_introduce_selffrom
3. custom actions
CustomAction,即自定義action,允許開發(fā)者執(zhí)行任何操作并反饋給用戶,比如簡單的返回一串字符串,或者控制家電、檢查銀行賬戶余額等等。它與DefaultAction不同,自定義action需要我們在domain.yml文件中的actions部分先進行定義,然后在指定的webserver中實現(xiàn)它,其中,這個webserver的url地址在endpoint.yml文件中指定,并且這個webserver可以通過任何語言實現(xiàn),當然這里首先推薦python來做,畢竟Rasa Core為我們封裝好了一個rasa-core-sdk專門用來處理自定義action。關于action web的搭建和action的具體實現(xiàn),我們在后面詳細講解,這里我們看下在在Rasa Core項目中需要做什么。假如我們在天氣資訊的人機對話系統(tǒng)需提供查詢天氣和空氣質量兩個業(yè)務,那么我們就需要在domain.yml文件中定義查詢天氣和空氣質量的action,即:
actions:
...
- action_search_weather
另外,F(xiàn)ormAction也是自定義actions,但是需要在domainl.yaml文件的forms字段聲明。
forms:
- weather_form
2.5 Policies
Policies是Rasa Core中的策略模塊,對應類rasa_core.policies.Policy,它的作用就是使用合適的策略(Policy)來預測一次對話后要執(zhí)行的行為(Actions)。預測的原理是衡量命中的哪些Policies哪個置信度高,由置信度高的Policy選擇合適的Action執(zhí)行。假如出現(xiàn)不同的Policy擁有相同的置信度,那么就由它們的優(yōu)先級決定,即選擇優(yōu)先級高的Policy。Rasa對提供的Policies進行了優(yōu)先級排序,具體如下表:
它們的描述與作用如下:
Memoization Policy
MemoizationPolicy只記住(memorizes)訓練數(shù)據(jù)中的對話。如果訓練數(shù)據(jù)中存在這樣的對話,那么它將以置信度為1.0預測下一個動作,否則將預測為None,此時置信度為0.0。下面演示了如何在策略配置文件config.yml文件中,配置MemoizationPlicy策略,其中,max_history(超參數(shù))決定了模型查看多少個對話歷史以決定下一個執(zhí)行的action。
policies:
- name: "MemoizationPolicy"
max_history: 5
注:max_history值越大訓練得到的模型就越大并且訓練時間會變長,關于該值到底該設置多少,我們可以舉這么個例子,比如有這么一個Intent:out_of_scope來描述用戶輸入的消息off-topic(離題),當用戶連續(xù)三次觸發(fā)out_of_scope意圖,這時候我們就需要主動告知用戶需要向其提供幫助,如果要Rasa Core能夠學習這種模型,max_history應該至少為3。story.md中表現(xiàn)如下:
* out_of_scope
- utter_default
* out_of_scope
- utter_default
* out_of_scope
- utter_help_message
Keras Policy
KerasPolicy策略是Keras框架中實現(xiàn)的神經網絡來預測選擇執(zhí)行下一個action,它默認的框架使用LSTM(Long Short-Term Memory,長短期記憶網絡)算法,但是我們也可以重寫KerasPolicy.model_architecture函數(shù)來實現(xiàn)自己的框架(architecture)。KerasPolicy的模型很簡單,只是單一的LSTM+Dense+softmax,這就需要我們不斷地完善自己的story來把各種情況下的story進行補充。下面演示了如何在策略配置文件config.yml文件中,配置KerasPolicy策略,其中,epochs表示訓練的次數(shù),max_history同上。
policies:
- name: KerasPolicy
epochs: 100
max_history: 5
Embedding Policy
基于機器學習的對話管理能夠學習復雜的行為以完成任務,但是將其功能擴展到新領域并不簡單,尤其是不同策略處理不合作用戶行為的能力,以及在學習新任務(如預訂酒店)時,如何將完成一項任務(如餐廳預訂)重新應用于該任務時的情況。EmbeddingPolicy,即循環(huán)嵌入式對話策略(Recurrent Embedding Dialogue Policy,REDP),它通過將actions和對話狀態(tài)嵌入到相同的向量空間(vector space)能夠獲得較好的效果,REDP包含一個基于改進的Neural Turing Machine的記憶組件和注意機制,在該任務上顯著優(yōu)于基線LSTM分類器。
EmbeddingPolicy效果上優(yōu)于KerasPolicy,但是它有個問題是耗時,因為它沒有使用GPU、沒有充分利用CPU資源。KerasPolicy和EmbeddingPolicy比較示意圖如下:
配置EmbeddingPolicy參數(shù):
policies:
- name: EmbeddingPolicy
epochs: 100
featurizer:
- name: FullDialogueTrackerFeaturizer
state_featurizer:
- name: LabelTokenizerSingleStateFeaturizer
注:新版的Rasa將EmbeddingPolicy重命名為TEDPolicy,但是我在config.yml配置文件中將其替換后,提示無法找到TEDPolicy異常,具體原因不明,暫還未涉及源碼分析。
Form Policy
FormPolicy是MemoizationPolicy的擴展,用于處理(form)表單的填充事項。當一個FormAction被調用時,F(xiàn)ormPolicy將持續(xù)預測表單動作,直到表單中的所有槽都被填滿,然后再執(zhí)行對應的FormAction。如果在Bot系統(tǒng)中使用了FormActions,就需要在config.yml配置文件中進行配置。
policies:
- name: FormPolicy
Mapping Policy
MappingPolicy可用于直接將意圖映射到要執(zhí)行的action,從而實現(xiàn)被映射的action總會被執(zhí)行,其中,這種映射是通過triggers屬性實現(xiàn)的。舉個栗子(domain.yml文件中):
intents:
- greet: {triggers: utter_goodbye}
其中,greet是意圖;utter_goodbye是action。一個意圖最多只能映射到一個action,我們的機器人一旦收到映射意圖的消息,它將執(zhí)行對應的action。然后,繼續(xù)監(jiān)聽下一條message。需要注意的是,對于上述映射,我們還需要要在story.md文件中添加如下樣本,否則,任何機器學習策略都可能被預測的action_greet在dialouge歷史中突然出現(xiàn)而混淆。
Fallback Policy
如果意圖識別的置信度低于nlu_threshold,或者沒有任何對話策略預測的action置信度高于core_threshold,F(xiàn)allbackPolicy將執(zhí)行fallback action。通俗來說,就是我們的對話機器人意圖識別和action預測的置信度沒有滿足對應的閾值,該策略將使機器人執(zhí)行指定的默認action。configs.yml配置如下:
policies:
- name: "FallbackPolicy"
# 意圖理解置信度閾值
nlu_threshold: 0.3
# action預測置信度閾值
core_threshold: 0.3
# fallback action
fallback_action_name: 'action_default_fallback'
其中,action_default_fallback是Rasa Core中的一個默認操作,它將向用戶發(fā)送utter_default模板消息,因此我們需要確保在domain.yml文件中指定此模板。當然,我們也可以在fallback_action_name字段自定義默認回復的action,比如my_fallback_cation,就可以這么改:
policies:
- name: "FallbackPolicy"
nlu_threshold: 0.4
core_threshold: 0.3
fallback_action_name: "my_fallback_action"
2.6 Slots
Slots,槽值,相當于機器人的內存(memory),它們以鍵值對的形式存在,用于存儲用戶輸入時消息時比較重要的信息,而這些信息將為Action的執(zhí)行提供關鍵數(shù)據(jù)。Slots的定義位于domain.yaml文件中,它們通常與Entities相對應,即Entities有哪些,Slots就有哪些,并且Slots存儲的值就是NLU模型提取的Entities的值。
2.6.1 Slots Type
1. Text類型
示例:
# domain.yaml
slots:
cuisine:
type: text
2. Boolean類型
示例:
slots:
is_authenticated:
type: bool
3. categorical類型
示例:
slots:
risk_level:
type: categorical
values:
- low
- medium
- high
4. Float類型
示例:
slots:
temperature:
type: float
min_value: -100.0
max_value: 100.0
5. List類型
示例:
slots:
shopping_items:
type: list
6. Unfeaturized 類型
示例:
slots:
internal_user_id:
type: unfeaturized
2.6.2 Slots Set
Slots值填充有多種方式,它們的操作方式如下:
1. Slots Initial
# domain.yaml
slots:
name:
type: text
initial_value: "human"
在domain.yaml文件中聲明slots時,可以通過initial_value字段為當前slot提供一個初始值,也就是說,當會話開始時,被設定初始值的slot已經被填充好。當然,這個操作不是必須的。
2. Slots Set from NLU
# stories.md
# story_01
* greet{"name": "Ali"}
- slot{"name": "Ali"}
- utter_greet
假如在stories.md文件添加一個包含-slot{}的story,這就意味著當NLU模型提取到一個名為name的實體且這個實體有在domain.yaml中定義,那么NLU模型提取到的實體值會被自動填充到name槽中。實際上,對于Rasa來說,就算你不添加-slot{}字段,這個實體值也會被提取并自動填充到name槽中。當然,如果你希望禁止這種自動填充行為,改為添加-slot{}字段填充,可以在domain.yaml定義slot時,設置auto_fill的值為False,即:
# domain.yaml
slots:
name:
type: text
auto_fill: False
3. Slots Set By Clicking Buttons
# domain.yaml
utter_ask_color:
- text: "what color would you like?"
buttons:
- title: "blue"
payload: '/choose{"color": "blue"}' # 格式 '/intent{"entity":"value",...}'
- title: "red"
payload: '/choose{"color": "red"}'
在點擊Button時填充Slots的值,是指當我們的Bot(Rasa Core)在回復用戶時,可以在回復的消息中附加Button信息,這種Button類似于快捷鍵,用戶獲取到之后,可以直接將其發(fā)送給Rasa Core,它會直接進行解析以識別intent和提取entity,并將entity的值填充到slot中。比如你讓用戶通過點擊一個按鈕來選擇一種顏色,那么可以在domain.yaml中utter_ask_color的回復中添加buttons:/choose{"color": "blue"}和/choose{"color": "red"}。注:通常每個button由title和payload字段組成。
4. Slots Set by Actions
from rasa_sdk.actions import Action
from rasa_sdk.events import SlotSet
import requests
class FetchProfileAction(Action):
def name(self):
return "fetch_profile"
def run(self, dispatcher, tracker, domain):
url = "http://myprofileurl.com"
data = requests.get(url).json
return [SlotSet("account_type", data["account_type"])]
該示例演示了如何在Custom Action中通過返回事件來填充Slots的值,即調用SlotSet事件函數(shù)并將該事件return。需要注意的是,為了達到這個目的,我們在編寫Story時必須包含該Slot,即使用-slot{}實現(xiàn),只有這樣Rasa Core就會從提供的信息中進行學習,并決定執(zhí)行正確的action。Story.md示例如下:
# story_01
* greet
- action_fetch_profile
- slot{"account_type" : "premium"}
- utter_welcome_premium
# story_02
* greet
- action_fetch_profile
- slot{"account_type" : "basic"}
- utter_welcome_basic
其中,account_type在domain.yaml中定義如下:
slots:
account_type:
type: categorical
values:
- premium
- basic
2.6.3 Slots Get
目前主要有兩種獲取Slots值方式:
1. Get Slot in responses
responses:
utter_greet:
- text: "Hey, {name}. How are you?"
在domain.yaml的responses部分,可以通過{slotname}的形式獲取槽值。
2. Get Slot in Custom Action
from rasa_sdk.actions import Action
from rasa_sdk.events import SlotSet
import requests
class FetchProfileAction(Action):
def name(self):
return "fetch_profile"
def run(self, dispatcher, tracker, domain):
# 獲取slot account_type的值
account_type = tracker.get_slot('account_type')
return
Tracker,可理解為跟蹤器,作用是以會話會話的形式維護助手和用戶之間的對話狀態(tài)。通過Tracker,能夠輕松獲取整個對話信息,其中就包括Slot的值。
2.7 Form
在Rasa Core中,當我們執(zhí)行一個action需要同時填充多個slot時,可以使用FormAction來實現(xiàn),因為FormAction會遍歷監(jiān)管的所有slot,當發(fā)現(xiàn)相關的slot未被填充時,就會向用戶主動發(fā)起詢問,直到所有slot被填充完畢,才會執(zhí)行接下來的業(yè)務邏輯。使用步驟如下:
(1)構造story
在story中,不僅需要考慮用戶按照我們的設計準確的提供有效信息,而且還要考慮用戶在中間過程改變要執(zhí)行的意圖情況或稱輸入無效信息,因為對于FormAction來說,如果無法獲得預期的信息就會報錯,這里我們分別稱這兩種情況為happy path、unhappy path。示例如下:
## happy path
* request_weather
- weather_form
- form{"name": "weather_form"} 激活form
- form{"name": } 使form無效
## unhappy path
* request_weather
- weather_form
- form{"name": "weather_form"}
* stop
- utter_ask_continue
* deny
- action_deactivate_form
- form{"name": }
注:* request_restaurant為意圖;- restaurant_form為form action;- form{"name": "restaurant_form"}為激活form;- form{"name": }為注銷form;- action_deactivate_form為默認的action,它的作用是用戶可能在表單操作過程中改變主意,決定不繼續(xù)最初的請求,我們使用這個default action來禁止(取消)表單,同時重置要請求的所有slots。
構建stroy最好使用官方提供的Interactive Learning,防止漏掉信息,詳細見本文2.8小節(jié)。
(2)添加form字段到Domain
在doamin文件下新增forms:部分,并將所有用到的form名稱添加到該字段下:
intents:
- request_weather
forms:
- weather_form
(3)配置FormPolicy
在工程的配置文件configs.yml中,新增FormPolicy策略:
policies:
- name: EmbeddingPolicy
epochs: 100
max_history: 5
- name: FallbackPolicy
fallback_action_name: 'action_default_fallback'
- name: MemoizationPolicy
max_history: 5
- name: FormPolicy
(4)Form Action實現(xiàn)
class WeatherForm(FormAction):
def name(self) -> Text:
"""Unique identifier of the form"""
return "weather_form"
@staticmethod
def required_slots(tracker: Tracker) -> List[Text]:
"""A list of required slots that the form has to fill"""
return ["date_time", "address"]
def submit(
self,
dispatcher: CollectingDispatcher,
tracker: Tracker,
domain: Dict[Text, Any],
) -> List[Dict]:
"""Define what the form has to do
after all required slots are filled"""
address = tracker.get_slot('address')
date_time = tracker.get_slot('date_time')
return
當form action第一被調用時,form就會被激活并進入FormPolicy策略模式。每次執(zhí)行form action,required_slots會被調用,當發(fā)現(xiàn)某個還未被填充時,會主動去調用形式為uter_ask_{slotname}的模板(注:定義在domain.yml的templates字段中);當所有slot被填充完畢,submit方法就會被調用,此時本次form操作完畢被取消激活。
2.8 Interactive Learning
雖然我們可以容易的人工構建story樣本數(shù)據(jù),但是往往會出現(xiàn)一些考慮不全,甚至出錯等問題,基于此,Rasa Core框架為我們提供了一種交互式學習(Interactive Learning)來獲得所需的樣本數(shù)據(jù)。在互動學習模式中,當你與機器人交談時,你會向它提供反饋,這是一種強大的方法來探索您的機器人可以做什么,也是修復它所犯錯誤的最簡單的方法?;跈C器學習的對話的一個優(yōu)點是,當你的機器人還不知道如何做某事時,你可以直接教它。
(1)開啟Action Server
python -m rasa run actions --port 5055 --actions actions --debug
(2)開啟Interactive Learning
python -m rasa interactive -m models/20200313-101055.tar.gz --endpoints configs/endpoints.yml --config configs/config.yml
# 或者(沒有已訓練模型情況)
# rasa會先訓練好模型,再開啟交互式學習會話
python -m rasa interactive --data /data --domain configs/domain.yml --endpoints configs/endpoints.yml --config configs/config.yml
分別執(zhí)行(1)、(2)命令后,我們可以預設一個交互場景根據(jù)終端的提示操作即可。如果一個交互場景所有流程執(zhí)行完畢,按Ctrl+C結束并選擇Start Fresh進入下一個場景即可。當然Rasa還提供了可視化界面,以幫助你了解每個Story樣本構建的過程,網址:http://localhost:5005/visualization.html。
執(zhí)行流程大致如下:
Bot loaded. Visualisation at http://localhost:5006/visualization.html .
Type a message and press enter (press 'Ctr-c' to exit).
? Your input -> 查詢身份證439912199008071234
? Is the intent 'request_idcard' correct for '查詢身份證[439912199008071234](id_number)' and are all entities labeled correctly? Yes
------
Chat History
# Bot You
───────────────────────────────────────────────────────────────────
1 action_listen
───────────────────────────────────────────────────────────────────
2 查詢身份證[439912199008071234](id_number)
intent: request_idcard 1.00
Current slots:
address: None, business: None, date-time: None, id_number: None, requested_slot: None
------
? The bot wants to run 'number_form', correct? Yes
Chat History
# Bot You
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
1 action_listen
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
2 查詢身份證[439912199008071234](id_number)
intent: request_idcard 1.00
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
3 number_form 1.00
您要查詢的身份證號碼439912199008071234所屬人為張三,湖南長沙人,現(xiàn)在就職于地球村物業(yè)。
form{"name": "number_form"}
slot{"id_number": "439912199008071234"}
form{"name": }
slot{"requested_slot": }
Current slots:
address: None, business: None, date-time: None, id_number: 439912199008071234, requested_slot: None
------
? The bot wants to run 'action_listen', correct? Yes
生成的一個Story示例如下:
## interactive_story_10
# unhappy path:chitchat stop but continue path
* greet
- utter_answer_greet
* request_number{"type": "身份證號碼"}
- number_form
- form{"name": "number_form"}
- slot{"type": "身份證號碼"}
- slot{"number": }
- slot{"business": }
- slot{"requested_slot": "number"}
* chitchat
- utter_chitchat
- number_form
- slot{"requested_slot": "number"}
* stop
- utter_ask_continue
* affirm
- number_form
- slot{"requested_slot": "number"}
* form: request_number{"number": "440123199087233467"}
- form: number_form
- slot{"number": "440123199087233467"}
- slot{"type": "身份證號碼"}
- form{"name": }
- slot{"requested_slot": }
* thanks
- utter_noworries
改進ChitChatAssistant項目
3.1 config.yml
# zh_jieba_mitie_embeddings_config.yml
language: "zh"
pipeline:
- name: "MitieNLP"
model: "data/total_word_feature_extractor_zh.dat"
- name: "JiebaTokenizer"
dictionary_path: "data/dict"
- name: "MitieEntityExtractor"
- name: "EntitySynonymMapper"
- name: "RegexFeaturizer"
- name: "MitieFeaturizer"
- name: "EmbeddingIntentClassifier"
policies:
- name: FallbackPolicy
nlu_threshold: 0.5
ambiguity_threshold: 0.1
core_threshold: 0.5
fallback_action_name: 'action_default_fallback'
- name: MemoizationPolicy
max_history: 5
- name: FormPolicy
- name: MappingPolicy
- name: EmbeddingPolicy
epochs: 500
考慮到目前項目的樣本較少,這里使用MITIE+EmbeddingPolicy組合,雖然訓練時慢了點,但是能夠保證實體提取的準確性,同時又能夠提高意圖識別的命中率。
3.2 weather_stories.md
## happy path
* request_weather
- weather_form
- form{"name": "weather_form"}
- form{"name": }
## happy path
* greet
- utter_answer_greet
* request_weather
- weather_form
- form{"name": "weather_form"}
- form{"name": }
* thanks
- utter_noworries
## unhappy path
* greet
- utter_answer_greet
* request_weather
- weather_form
- form{"name": "weather_form"}
* chitchat
- utter_chitchat
- weather_form
- form{"name": }
* thanks
- utter_noworries
## very unhappy path
* greet
- utter_answer_greet
* request_weather
- weather_form
- form{"name": "weather_form"}
* chitchat
- utter_chitchat
- weather_form
* chitchat
- utter_chitchat
- weather_form
* chitchat
- utter_chitchat
- weather_form
- form{"name": }
* thanks
- utter_noworries
## stop but continue path
* greet
- utter_answer_greet
* request_weather
- weather_form
- form{"name": "weather_form"}
* stop
- utter_ask_continue
* affirm
- weather_form
- form{"name": }
* thanks
- utter_noworries
## stop and really stop path
* greet
- utter_answer_greet
* request_weather
- weather_form
- form{"name": "weather_form"}
* stop
- utter_ask_continue
* deny
- action_deactivate_form
- form{"name": }
## chitchat stop but continue path
* request_weather
- weather_form
- form{"name": "weather_form"}
* chitchat
- utter_chitchat
- weather_form
* stop
- utter_ask_continue
* affirm
- weather_form
- form{"name": }
* thanks
- utter_noworries
## stop but continue and chitchat path
* greet
- utter_answer_greet
* request_weather
- weather_form
- form{"name": "weather_form"}
* stop
- utter_ask_continue
* affirm
- weather_form
* chitchat
- utter_chitchat
- weather_form
- form{"name": }
* thanks
- utter_noworries
## chitchat stop but continue and chitchat path
* greet
- utter_answer_greet
* request_weather
- weather_form
- form{"name": "weather_form"}
* chitchat
- utter_chitchat
- weather_form
* stop
- utter_ask_continue
* affirm
- weather_form
* chitchat
- utter_chitchat
- weather_form
- form{"name": }
* thanks
- utter_noworries
## chitchat, stop and really stop path
* greet
- utter_answer_greet
* request_weather
- weather_form
- form{"name": "weather_form"}
* chitchat
- utter_chitchat
- weather_form
* stop
- utter_ask_continue
* deny
- action_deactivate_form
- form{"name": }
## interactive_story_1
## 天氣 + 時間 + 地點 + 地點
* request_weather
- weather_form
- form{"name": "weather_form"}
- slot{"requested_slot": "date_time"}
* form: inform{"date_time": "明天"}
- form: weather_form
- slot{"date_time": "明天"}
- slot{"requested_slot": "address"}
* form: inform{"address": "廣州"}
- form: weather_form
- slot{"address": "廣州"}
- form{"name": }
- slot{"requested_slot": }
* inform{"date_time": "后天"} OR request_weather{"date_time": "后天"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "明天"}
- slot{"address": "廣州"}
- slot{"date_time": "后天"}
- form{"name": }
- slot{"requested_slot": }
* thanks
- utter_answer_thanks
## interactive_story_1
## 天氣 + 時間 + 地點 + 時間
* request_weather
- weather_form
- form{"name": "weather_form"}
- slot{"requested_slot": "date_time"}
* form: inform{"date_time": "明天"}
- form: weather_form
- slot{"date_time": "明天"}
- slot{"requested_slot": "address"}
* form: inform{"address": "廣州"}
- form: weather_form
- slot{"address": "廣州"}
- form{"name": }
- slot{"requested_slot": }
* inform{"address": "上海"} OR request_weather{"address": "深圳"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "明天"}
- slot{"address": "廣州"}
- slot{"address": "上海"}
- form{"name": }
- slot{"requested_slot": }
* affirm
- utter_answer_affirm
## interactive_story_2
## 天氣/時間/地點 + 地點
* request_weather{"date_time": "明天", "address": "上海"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "明天"}
- slot{"address": "上海"}
- form{"name": }
- slot{"requested_slot": }
* inform{"address": "廣州"} OR request_weather{"address": "廣州"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "明天"}
- slot{"address": "上海"}
- slot{"address": "廣州"}
- form{"name": }
- slot{"requested_slot": }
* thanks
- utter_answer_thanks
## interactive_story_3
## 天氣/時間/地點 + 時間
* request_weather{"address": "深圳", "date_time": "后天"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "后天"}
- slot{"address": "深圳"}
- form{"name": }
- slot{"requested_slot": }
* inform{"date_time": "大后天"} OR request_weather{"date_time": "大后天"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "后天"}
- slot{"address": "深圳"}
- slot{"date_time": "大后天"}
- form{"name": }
- slot{"requested_slot": }
* thanks
- utter_answer_thanks
## interactive_story_2
## 天氣/時間/地點 + 地點 + 時間
* request_weather{"date_time": "明天", "address": "上海"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "明天"}
- slot{"address": "上海"}
- form{"name": }
- slot{"requested_slot": }
* inform{"address": "北京"} OR request_weather{"address": "北京"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "明天"}
- slot{"address": "上海"}
- slot{"address": "北京"}
- form{"name": }
- slot{"requested_slot": }
* inform{"date_time": "后天"} OR request_weather{"date_time": "后天"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "明天"}
- slot{"address": "北京"}
- slot{"date_time": "后天"}
- form{"name": }
- slot{"requested_slot": }
* affirm
- utter_answer_affirm
## interactive_story_3
## 天氣/時間/地點 + 地點 + 地點
* request_weather{"date_time": "后天", "address": "北京"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "后天"}
- slot{"address": "北京"}
- form{"name": }
- slot{"requested_slot": }
* inform{"address": "深圳"} OR request_weather{"address": "深圳"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "后天"}
- slot{"address": "北京"}
- slot{"address": "深圳"}
- form{"name": }
- slot{"requested_slot": }
* inform{"address": "南京"} OR request_weather{"address": "南京"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "后天"}
- slot{"address": "深圳"}
- slot{"address": "南京"}
- form{"name": }
- slot{"requested_slot": }
* thanks
- utter_answer_thanks
## interactive_story_4
## 天氣/時間/地點 + 時間 + 地點
* request_weather{"date_time": "明天", "address": "長沙"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "明天"}
- slot{"address": "長沙"}
- form{"name": }
- slot{"requested_slot": }
* inform{"date_time": "后天"} OR request_weather{"date_time": "后天"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "明天"}
- slot{"address": "長沙"}
- slot{"date_time": "后天"}
- form{"name": }
- slot{"requested_slot": }
* inform{"date_time": "大后天"} OR request_weather{"date_time": "大后天"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "后天"}
- slot{"address": "長沙"}
- slot{"date_time": "大后天"}
- form{"name": }
- slot{"requested_slot": }
* affirm
- utter_answer_affirm
## interactive_story_5
## 天氣/時間/地點 + 時間 + 時間
* request_weather{"date_time": "后天", "address": "深圳"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "后天"}
- slot{"address": "深圳"}
- form{"name": }
- slot{"requested_slot": }
* inform{"date_time": "明天"} OR request_weather{"date_time": "明天"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "后天"}
- slot{"address": "深圳"}
- slot{"date_time": "明天"}
- form{"name": }
- slot{"requested_slot": }
* inform{"address": "廣州"} OR request_weather{"address": "廣州"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "明天"}
- slot{"address": "深圳"}
- slot{"address": "廣州"}
- form{"name": }
- slot{"requested_slot": }
* thanks
- utter_answer_thanks
## interactive_story_4
## 天氣/時間 + 地點 + 時間
* request_weather{"date_time": "明天"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "明天"}
- slot{"requested_slot": "address"}
* form: inform{"address": "廣州"}
- form: weather_form
- slot{"address": "廣州"}
- form{"name": }
- slot{"requested_slot": }
* inform{"date_time": "后天"} OR request_weather{"date_time": "后天"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "明天"}
- slot{"address": "廣州"}
- slot{"date_time": "后天"}
- form{"name": }
- slot{"requested_slot": }
* thanks
- utter_answer_thanks
## interactive_story_5
## 天氣/地點 + 時間 + 時間
* request_weather{"address": "廣州"}
- weather_form
- form{"name": "weather_form"}
- slot{"address": "廣州"}
- slot{"requested_slot": "date_time"}
* form: inform{"date_time": "后天"}
- form: weather_form
- slot{"date_time": "后天"}
- form{"name": }
- slot{"requested_slot": }
* inform{"date_time": "明天"} OR request_weather{"date_time": "明天"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "后天"}
- slot{"address": "廣州"}
- slot{"date_time": "明天"}
- form{"name": }
- slot{"requested_slot": }
* thanks
- utter_answer_thanks
## interactive_story_1
## 天氣/時間/地點 + chit + chit(restart)+詢問天氣
* request_weather{"date_time": "今天", "address": "廣州"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "今天"}
- slot{"address": "廣州"}
- form{"name": }
- slot{"requested_slot": }
* chitchat
- utter_chitchat
* chitchat
- utter_chitchat
- action_restart
* request_weather
- weather_form
- form{"name": "weather_form"}
- slot{"requested_slot": "date_time"}
* form: inform{"date_time": "今天"}
- form: weather_form
- slot{"date_time": "今天"}
- slot{"requested_slot": "address"}
* form: inform{"address": "廣州"}
- form: weather_form
- slot{"address": "廣州"}
- form{"name": }
- slot{"requested_slot": }
* thanks
- utter_answer_thanks
在構建Story樣本時,主要是使用Interactive Learning工具實現(xiàn),以確保枚舉盡可能多的unhappy story,同時又能夠防止在構建樣本時出現(xiàn)信息遺漏的情況。此外,本版本中除了查詢天氣這個案例,還新增了其他案例,并列舉了如何使用同義詞、自定義字典以及正則表達式的使用方法,詳細見最新版項目。
GitHub地址:ChitChatAssistant https://github.com/jiangdongguo/ChitChatAssistant,歡迎star和issues,我們共同討論、學習!
原文鏈接:
https://blog.csdn.net/AndrExpert/article/details/105434136
?沒有監(jiān)控和日志咋整?老程序員來支招
?朱廣權李佳琦直播掉線,1.2億人在線等
?RPC的超時設置,一不小心就是線上事故!
?拿下Gartner容器產品第一,阿里云打贏云原生關鍵一戰(zhàn)!
?深聊Solidity的測試場景、方法和實踐,太詳細了,必須收藏!
?萬字干貨:一步步教你如何在容器上構建持續(xù)部署!
?據(jù)說,這是當代極客們的【技術風向標】...
今日福利:評論區(qū)留言入選,可獲得價值299元的「2020 AI開發(fā)者萬人大會」在線直播門票一張。 快來動動手指,寫下你想說的話吧。
文檔對象模型 (DOM) 是HTML和XML文檔的編程接口。它提供了對文檔的結構化的表述,并定義了一種方式可以使從程序中對該結構進行訪問,從而改變文檔的結構,樣式和內容。文檔對象模型 (DOM) 是對HTML文件的另一種展示,通俗地說,一個HTML 文件,我們可以用編輯器以代碼的形式展示它,也可以用瀏覽器以頁面的形式展示它,同一份文件通過不同的展示方式,就有了不一樣的表現(xiàn)形式。而DOM 將文檔解析為一個由節(jié)點和對象(包含屬性和方法的對象)組成的結構集合。簡言之,它會將web頁面和腳本或程序語言連接起來,我們可以使用腳本或者程序語言通過DOM 來改變或者控制web頁面。
我們可以通過JavaScript 來調用document和window元素的API來操作文檔或者獲取文檔的信息。
Node 是一個接口,有許多接口都從Node 繼承方法和屬性:Document, Element, CharacterData (which Text, Comment, and CDATASection inherit), ProcessingInstruction, DocumentFragment, DocumentType, Notation, Entity, EntityReference。Node 有一個nodeType的屬性表示Node 的類型,是一個整數(shù),不同的值代表不同的節(jié)點類型。具體如下表所示:
節(jié)點類型常量
已棄用的節(jié)點類型常量
假設我們要判斷一個Node 是不是一個元素,通過查表可知元素的nodeType屬性值為1,代碼可以這么寫:
復制代碼if(X.nodeType === 1){
console.log('X 是一個元素');
}
在Node 類型中,比較常用的就是element,text,comment,document,document_fragment這幾種類型。
Element提供了對元素標簽名,子節(jié)點和特性的訪問,我們常用HTML元素比如div,span,a等標簽就是element中的一種。Element有下面幾條特性:(1)nodeType為1(2)nodeName為元素標簽名,tagName也是返回標簽名(3)nodeValue為null(4)parentNode可能是Document或Element(5)子節(jié)點可能是Element,Text,Comment,Processing_Instruction,CDATASection或EntityReference
Text表示文本節(jié)點,它包含的是純文本內容,不能包含html代碼,但可以包含轉義后的html代碼。Text有下面的特性:(1)nodeType為3(2)nodeName為#text(3)nodeValue為文本內容(4)parentNode是一個Element(5)沒有子節(jié)點
Comment表示HTML文檔中的注釋,它有下面的幾種特征:(1)nodeType為8(2)nodeName為#comment(3)nodeValue為注釋的內容(4)parentNode可能是Document或Element(5)沒有子節(jié)點
Document表示文檔,在瀏覽器中,document對象是HTMLDocument的一個實例,表示整個頁面,它同時也是window對象的一個屬性。Document有下面的特性:(1)nodeType為9(2)nodeName為#document(3)nodeValue為null(4)parentNode為null(5)子節(jié)點可能是一個DocumentType或Element
DocumentFragment是所有節(jié)點中唯一一個沒有對應標記的類型,它表示一種輕量級的文檔,可能當作一個臨時的倉庫用來保存可能會添加到文檔中的節(jié)點。DocumentFragment有下面的特性:(1)nodeType為11(2)nodeName為#document-fragment(3)nodeValue為null(4)parentNode為null
用如其名,這類API是用來創(chuàng)建節(jié)點的
createElement通過傳入指定的一個標簽名來創(chuàng)建一個元素,如果傳入的標簽名是一個未知的,則會創(chuàng)建一個自定義的標簽,注意:IE8以下瀏覽器不支持自定義標簽。
語法
復制代碼 let element = document.createElement(tagName);
使用createElement要注意:通過createElement創(chuàng)建的元素并不屬于HTML文檔,它只是創(chuàng)建出來,并未添加到HTML文檔中,要調用appendChild或insertBefore等方法將其添加到HTML文檔樹中。
例子:
復制代碼 let elem = document.createElement("div");
elem.id = 'test';
elem.style = 'color: red';
elem.innerHTML = '我是新創(chuàng)建的節(jié)點';
document.body.appendChild(elem);
運行結果為:
createTextNode用來創(chuàng)建一個文本節(jié)點
語法
復制代碼 var text = document.createTextNode(data);
createTextNode接收一個參數(shù),這個參數(shù)就是文本節(jié)點中的文本,和createElement一樣,創(chuàng)建后的文本節(jié)點也只是獨立的一個節(jié)點,同樣需要appendChild將其添加到HTML文檔樹中
例子:
復制代碼 var node = document.createTextNode("我是文本節(jié)點");
document.body.appendChild(node);
運行結果為:
cloneNode返回調用該方法的節(jié)點的一個副本
語法
復制代碼 var dupNode = node.cloneNode(deep);
node 將要被克隆的節(jié)點dupNode 克隆生成的副本節(jié)點deep(可選)是否采用深度克隆,如果為true,則該節(jié)點的所有后代節(jié)點也都會被克隆,如果為false,則只克隆該節(jié)點本身.
這里有幾點要注意:(1)和createElement一樣,cloneNode創(chuàng)建的節(jié)點只是游離有HTML文檔外的節(jié)點,要調用appendChild方法才能添加到文檔樹中(2)如果復制的元素有id,則其副本同樣會包含該id,由于id具有唯一性,所以在復制節(jié)點后必須要修改其id(3)調用接收的deep參數(shù)最好傳入,如果不傳入該參數(shù),不同瀏覽器對其默認值的處理可能不同
注意如果被復制的節(jié)點綁定了事件,則副本也會跟著綁定該事件嗎?這里要分情況討論:(1)如果是通過addEventListener或者比如onclick進行綁定事件,則副本節(jié)點不會綁定該事件(2)如果是內聯(lián)方式綁定比如:<div onclick="showParent()"></div>,這樣的話,副本節(jié)點同樣會觸發(fā)事件。
例子:
復制代碼<body>
<div id="parent">
我是父元素的文本
<br/>
<span>
我是子元素
</span>
</div>
<button id="btnCopy">復制</button>
</body>
<script>
var parent = document.getElementById("parent");
document.getElementById("btnCopy").onclick = function(){
var parent2 = parent.cloneNode(true);
parent2.id = "parent2";
document.body.appendChild(parent2);
}
</script>
運行結果為:
DocumentFragments 是DOM節(jié)點。它們不是主DOM樹的一部分。通常的用例是創(chuàng)建文檔片段,將元素附加到文檔片段,然后將文檔片段附加到DOM樹。在DOM樹中,文檔片段被其所有的子元素所代替。因為文檔片段存在于內存中,并不在DOM樹中,所以將子元素插入到文檔片段時不會引起頁面回流(reflow)(對元素位置和幾何上的計算)。因此,使用文檔片段document fragments 通常會起到優(yōu)化性能的作用。
語法
復制代碼 let fragment = document.createDocumentFragment();
例子:
復制代碼<body>
<ul id="ul"></ul>
</body>
<script>
(function()
{
var start = Date.now();
var str = '', li;
var ul = document.getElementById('ul');
var fragment = document.createDocumentFragment();
for(var i=0; i<1000; i++)
{
li = document.createElement('li');
li.textContent = '第'+(i+1)+'個子節(jié)點';
fragment.appendChild(li);
}
ul.appendChild(fragment);
})();
</script>
運行結果為:
節(jié)點創(chuàng)建型API主要包括createElement,createTextNode,cloneNode和createDocumentFragment四個方法,需要注意下面幾點:(1)它們創(chuàng)建的節(jié)點只是一個孤立的節(jié)點,要通過appendChild添加到文檔中(2)cloneNode要注意如果被復制的節(jié)點是否包含子節(jié)點以及事件綁定等問題(3)使用createDocumentFragment來解決添加大量節(jié)點時的性能問題
前面我們提到節(jié)點創(chuàng)建型API,它們只是創(chuàng)建節(jié)點,并沒有真正修改到頁面內容,而是要調用·appendChild·來將其添加到文檔樹中。我在這里將這類會修改到頁面內容歸為一類。修改頁面內容的api主要包括:appendChild,insertBefore,removeChild,replaceChild。
appendChild我們在前面已經用到多次,就是將指定的節(jié)點添加到調用該方法的節(jié)點的子元素的末尾。
語法
復制代碼 parent.appendChild(child);
child節(jié)點將會作為parent節(jié)點的最后一個子節(jié)點。appendChild這個方法很簡單,但是還有有一點需要注意:如果被添加的節(jié)點是一個頁面中存在的節(jié)點,則執(zhí)行后這個節(jié)點將會添加到指定位置,其原本所在的位置將移除該節(jié)點,也就是說不會同時存在兩個該節(jié)點在頁面上,相當于把這個節(jié)點移動到另一個地方。如果child綁定了事件,被移動時,它依然綁定著該事件。
例子:
復制代碼<body>
<div id="child">
要被添加的節(jié)點
</div>
<br/>
<br/>
<br/>
<div id="parent">
要移動的位置
</div>
<input id="btnMove" type="button" value="移動節(jié)點" />
</body>
<script>
document.getElementById("btnMove").onclick = function(){
var child = document.getElementById("child");
document.getElementById("parent").appendChild(child);
}
</script>
運行結果:
insertBefore用來添加一個節(jié)點到一個參照節(jié)點之前
語法
復制代碼 parentNode.insertBefore(newNode,refNode);
parentNode表示新節(jié)點被添加后的父節(jié)點newNode表示要添加的節(jié)點refNode表示參照節(jié)點,新節(jié)點會添加到這個節(jié)點之前
例子:
復制代碼<body>
<div id="parent">
父節(jié)點
<div id="child">
子元素
</div>
</div>
<input type="button" id="insertNode" value="插入節(jié)點" />
</body>
<script>
var parent = document.getElementById("parent");
var child = document.getElementById("child");
document.getElementById("insertNode").onclick = function(){
var newNode = document.createElement("div");
newNode.textContent = "新節(jié)點"
parent.insertBefore(newNode,child);
}
</script>
運行結果:
關于第二個參數(shù)參照節(jié)點還有幾個注意的地方:(1)refNode是必傳的,如果不傳該參數(shù)會報錯(2)如果refNode是undefined或null,則insertBefore會將節(jié)點添加到子元素的末尾
刪除指定的子節(jié)點并返回
語法
復制代碼 var deletedChild = parent.removeChild(node);
deletedChild指向被刪除節(jié)點的引用,它等于node,被刪除的節(jié)點仍然存在于內存中,可以對其進行下一步操作。注意:如果被刪除的節(jié)點不是其子節(jié)點,則程序將會報錯。我們可以通過下面的方式來確??梢詣h除:
復制代碼if(node.parentNode){
node.parentNode.removeChild(node);
}
運行結果:
通過節(jié)點自己獲取節(jié)點的父節(jié)點,然后將自身刪除
replaceChild用于使用一個節(jié)點替換另一個節(jié)點
語法
復制代碼 parent.replaceChild(newChild,oldChild);
newChild是替換的節(jié)點,可以是新的節(jié)點,也可以是頁面上的節(jié)點,如果是頁面上的節(jié)點,則其將被轉移到新的位置oldChild是被替換的節(jié)點
例子:
復制代碼<body>
<div id="parent">
父節(jié)點
<div id="child">
子元素
</div>
</div>
<input type="button" id="insertNode" value="替換節(jié)點" />
</body>
<script>
var parent = document.getElementById("parent");
var child = document.getElementById("child");
document.getElementById("insertNode").onclick = function(){
var newNode = document.createElement("div");
newNode.textContent = "新節(jié)點"
parent.replaceChild(newNode,child)
}
運行結果:
頁面修改型API主要是這四個接口,要注意幾個特點:(1)不管是新增還是替換節(jié)點,如果新增或替換的節(jié)點是原本存在頁面上的,則其原來位置的節(jié)點將被移除,也就是說同一個節(jié)點不能存在于頁面的多個位置(2)節(jié)點本身綁定的事件會不會消失,會一直保留著。
這個接口很簡單,根據(jù)元素id返回元素,返回值是Element類型,如果不存在該元素,則返回null
語法
復制代碼 var element = document.getElementById(id);
使用這個接口有幾點要注意:(1)元素的Id是大小寫敏感的,一定要寫對元素的id(2)HTML文檔中可能存在多個id相同的元素,則返回第一個元素(3)只從文檔中進行搜索元素,如果創(chuàng)建了一個元素并指定id,但并沒有添加到文檔中,則這個元素是不會被查找到的
例子:
復制代碼<body>
<p id="para1">Some text here</p>
<button onclick="changeColor('blue');">blue</button>
<button onclick="changeColor('red');">red</button>
</body>
<script>
function changeColor(newColor) {
var elem = document.getElementById("para1");
elem.style.color = newColor;
}
</script>
運行結果:
返回一個包括所有給定標簽名稱的元素的HTML集合HTMLCollection。 整個文件結構都會被搜索,包括根節(jié)點。返回的 HTML集合是動態(tài)的, 意味著它可以自動更新自己來保持和 DOM 樹的同步而不用再次調用document.getElementsByTagName()
語法
復制代碼 var elements = document.getElementsByTagName(name);
(1)如果要對HTMLCollection集合進行循環(huán)操作,最好將其長度緩存起來,因為每次循環(huán)都會去計算長度,暫時緩存起來可以提高效率(2)如果沒有存在指定的標簽,該接口返回的不是null,而是一個空的HTMLCollection(3)name是一個代表元素的名稱的字符串。特殊字符 "*" 代表了所有元素。
例子:
復制代碼<body>
<div>div1</div>
<div>div2</div>
<input type="button" value="顯示數(shù)量" id="btnShowCount"/>
<input type="button" value="新增div" id="btnAddDiv"/>
</body>
<script>
var divList = document.getElementsByTagName("div");
document.getElementById("btnAddDiv").onclick = function(){
var div = document.createElement("div");
div.textContent ="div" + (divList.length+1);
document.body.appendChild(div);
}
document.getElementById("btnShowCount").onclick = function(){
alert(divList.length);
}
</script>
這段代碼中有兩個按鈕,一個按鈕是顯示HTMLCollection元素的個數(shù),另一個按鈕可以新增一個div標簽到文檔中。前面提到HTMLCollcetion元素是即時的表示該集合是隨時變化的,也就是是文檔中有幾個div,它會隨時進行變化,當我們新增一個div后,再訪問HTMLCollection時,就會包含這個新增的div。
運行結果:
getElementsByName主要是通過指定的name屬性來獲取元素,它返回一個即時的NodeList對象
語法
復制代碼 var elements = document.getElementsByName(name)
使用這個接口主要要注意幾點:(1)返回對象是一個即時的NodeList,它是隨時變化的(2)在HTML元素中,并不是所有元素都有name屬性,比如div是沒有name屬性的,但是如果強制設置div的name屬性,它也是可以被查找到的(3)在IE中,如果id設置成某個值,然后傳入getElementsByName的參數(shù)值和id值一樣,則這個元素是會被找到的,所以最好不好設置同樣的值給id和name
例子:
復制代碼<script type="text/javascript">
function getElements()
{
var x=document.getElementsByName("myInput");
alert(x.length);
}
</script>
<body>
<input name="myInput" type="text" size="20" /><br />
<input name="myInput" type="text" size="20" /><br />
<input name="myInput" type="text" size="20" /><br />
<br />
<input type="button" onclick="getElements()" value="How many elements named 'myInput'?" />
</body>
運行結果:
這個API是根據(jù)元素的class返回一個即時的HTMLCollection
語法
復制代碼 var elements = document.getElementsByClassName(names); // or:
var elements = rootElement.getElementsByClassName(names);
這個接口有下面幾點要注意:(1)返回結果是一個即時的HTMLCollection,會隨時根據(jù)文檔結構變化(2)IE9以下瀏覽器不支持(3)如果要獲取2個以上classname,可傳入多個classname,每個用空格相隔,例如
復制代碼 var elements = document.getElementsByClassName("test1 test2");
例子:
復制代碼 var elements = document.getElementsByClassName('test');
復制代碼 var elements = document.getElementsByClassName('red test');
復制代碼 var elements = document.getElementById('main').getElementsByClassName('test');
復制代碼 var testElements = document.getElementsByClassName('test');
var testDivs = Array.prototype.filter.call(testElements, function(testElement){
return testElement.nodeName === 'DIV';;
});
這兩個API很相似,通過css選擇器來查找元素,注意選擇器要符合CSS選擇器的規(guī)則
document.querySelector返回第一個匹配的元素,如果沒有匹配的元素,則返回null
語法
復制代碼 var element = document.querySelector(selectors);
注意,由于返回的是第一個匹配的元素,這個api使用的深度優(yōu)先搜索來獲取元素。
例子:
復制代碼<body>
<div>
<div>
<span class="test">第三級的span</span>
</div>
</div>
<div class="test">
同級的第二個div
</div>
<input type="button" id="btnGet" value="獲取test元素" />
</body>
<script>
document.getElementById("btnGet").addEventListener("click",function(){
var element = document.querySelector(".test");
alert(element.textContent);
})
</script>
兩個class都包含“test”的元素,一個在文檔樹的前面,但是它在第三級,另一個在文檔樹的后面,但它在第一級,通過querySelector獲取元素時,它通過深度優(yōu)先搜索,拿到文檔樹前面的第三級的元素。運行結果:
語法
復制代碼 var elementList = document.querySelectorAll(selectors);
例子:
復制代碼 var matches = document.querySelectorAll("div.note, div.alert");
返回一個文檔中所有的class為"note"或者"alert"的div元素
復制代碼<body>
<div class="test">
class為test
</div>
<div id="test">
id為test
</div>
<input id="btnShow" type="button" value="顯示內容" />
</body>
<script>
document.getElementById("btnShow").addEventListener("click",function(){
var elements = document.querySelectorAll("#test,.test");
for(var i = 0,length = elements.length;i<length;i++){
alert(elements[i].textContent);
}
})
</script>
這段代碼通過querySelectorAll,使用id選擇器和class選擇器選擇了兩個元素,并依次輸出其內容。要注意兩點:(1)querySelectorAll也是通過深度優(yōu)先搜索,搜索的元素順序和選擇器的順序無關(2)返回的是一個非即時的NodeList,也就是說結果不會隨著文檔樹的變化而變化兼容性問題:querySelector和querySelectorAll在ie8以下的瀏覽器不支持。
運行結果:
在html文檔中的每個節(jié)點之間的關系都可以看成是家譜關系,包含父子關系,兄弟關系等等
每個節(jié)點都有一個parentNode屬性,它表示元素的父節(jié)點。Element的父節(jié)點可能是Element,Document或DocumentFragment
返回元素的父元素節(jié)點,與parentNode的區(qū)別在于,其父節(jié)點必須是一個Element,如果不是,則返回null
返回一個即時的NodeList,表示元素的子節(jié)點列表,子節(jié)點可能會包含文本節(jié)點,注釋節(jié)點等
一個即時的HTMLCollection,子節(jié)點都是Element,IE9以下瀏覽器不支持children屬性為只讀屬性,對象類型為HTMLCollection,你可以使用elementNodeReference.children[1].nodeName來獲取某個子元素的標簽名稱
只讀屬性返回樹中節(jié)點的第一個子節(jié)點,如果節(jié)點是無子節(jié)點,則返回 null
返回當前節(jié)點的最后一個子節(jié)點。如果父節(jié)點為一個元素節(jié)點,則子節(jié)點通常為一個元素節(jié)點,或一個文本節(jié)點,或一個注釋節(jié)點。如果沒有子節(jié)點,則返回null
返回一個布爾值,表明當前節(jié)點是否包含有子節(jié)點.
返回當前節(jié)點的前一個兄弟節(jié)點,沒有則返回nullGecko內核的瀏覽器會在源代碼中標簽內部有空白符的地方插入一個文本結點到文檔中.因此,使用諸如Node.firstChild和Node.previousSibling之類的方法可能會引用到一個空白符文本節(jié)點, 而不是使用者所預期得到的節(jié)點
previousElementSibling返回當前元素在其父元素的子元素節(jié)點中的前一個元素節(jié)點,如果該元素已經是第一個元素節(jié)點,則返回null,該屬性是只讀的。注意IE9以下瀏覽器不支持
Node.nextSibling是一個只讀屬性,返回其父節(jié)點的childNodes列表中緊跟在其后面的節(jié)點,如果指定的節(jié)點為最后一個節(jié)點,則返回nullGecko內核的瀏覽器會在源代碼中標簽內部有空白符的地方插入一個文本結點到文檔中.因此,使用諸如Node.firstChild和Node.previousSibling之類的方法可能會引用到一個空白符文本節(jié)點, 而不是使用者所預期得到的節(jié)點
nextElementSibling返回當前元素在其父元素的子元素節(jié)點中的后一個元素節(jié)點,如果該元素已經是最后一個元素節(jié)點,則返回null,該屬性是只讀的。注意IE9以下瀏覽器不支持
設置指定元素上的一個屬性值。如果屬性已經存在,則更新該值; 否則將添加一個新的屬性用指定的名稱和值
語法
復制代碼 element.setAttribute(name, value);
其中name是特性名,value是特性值。如果元素不包含該特性,則會創(chuàng)建該特性并賦值。
例子:
復制代碼<body>
<div id="div1">ABC</div>
</body>
<script>
let div1 = document.getElementById("div1");
div1.setAttribute("align", "center");
</script>
運行結果:
如果元素本身包含指定的特性名為屬性,則可以世界訪問屬性進行賦值,比如下面兩條代碼是等價的:
復制代碼 element.setAttribute("id","test");
element.id = "test";
getAttribute()返回元素上一個指定的屬性值。如果指定的屬性不存在,則返回null或""(空字符串)
語法
復制代碼 let attribute = element.getAttribute(attributeName);
attribute是一個包含attributeName屬性值的字符串。attributeName是你想要獲取的屬性值的屬性名稱
例子:
復制代碼<body>
<div id="div1">ABC</div>
</body>
<script>
let div1 = document.getElementById("div1");
let align = div1.getAttribute("align");
alert(align);
</script>
運行結果:
removeAttribute()從指定的元素中刪除一個屬性
語法
復制代碼 element.removeAttribute(attrName)
attrName是一個字符串,將要從元素中刪除的屬性名
例子:
復制代碼<body>
<div id="div1" style="color:red" width="200px">ABC
</div>
</body>
<script>
let div = document.getElementById("div1")
div.removeAttribute("style");
</script>
在運行之前div有個style="color:red"的屬性,在運行之后這個屬性就被刪除了
運行結果:
Window.getComputedStyle()方法給出應用活動樣式表后的元素的所有CSS屬性的值,并解析這些值可能包含的任何基本計算假設某個元素并未設置高度而是通過其內容將其高度撐開,這時候要獲取它的高度就要用到getComputedStyle
語法
復制代碼 var style = window.getComputedStyle(element[, pseudoElt]);
element是要獲取的元素,pseudoElt指定一個偽元素進行匹配。返回的style是一個CSSStyleDeclaration對象。通過style可以訪問到元素計算后的樣式
getBoundingClientRect用來返回元素的大小以及相對于瀏覽器可視窗口的位置
語法
復制代碼 var clientRect = element.getBoundingClientRect();
clientRect是一個DOMRect對象,包含left,top,right,bottom,它是相對于可視窗口的距離,滾動位置發(fā)生改變時,它們的值是會發(fā)生變化的。除了IE9以下瀏覽器,還包含元素的height和width等數(shù)據(jù)
例子:
復制代碼 elem.style.color = 'red';
elem.style.setProperty('font-size', '16px');
elem.style.removeProperty('color');
例子:
復制代碼 var style = document.createElement('style');
style.innerHTML = 'body{color:red} #top:hover{background-color: red;color: white;}';
document.head.appendChild(style););
JavaScript中的API太多了,將這些API記住并熟練使用對JavaScript的學習是有很大的幫助
作者:yyzclyang
鏈接:https://juejin.cn/post/6844903604445249543
*請認真填寫需求信息,我們會在24小時內與您取得聯(lián)系。