nlu模塊的主要功能是解析用戶輸入數(shù)據(jù),識(shí)別出用戶輸入的實(shí)體丰榴、意圖等關(guān)鍵信息货邓,同時(shí)也可以添加諸如情感分析等自定義模塊。
一四濒、輸入數(shù)據(jù)
nlu模塊接受Message類型的數(shù)據(jù)作為輸入换况,與core模塊流轉(zhuǎn)的Usermessage數(shù)據(jù)不同职辨,Message定義在rasa/nlu/training_data/message.py中,默認(rèn)有三個(gè)變量复隆,分別為text拨匆、time、data挽拂。其中惭每,text中存儲(chǔ)的是用戶輸入的問(wèn)題,time存儲(chǔ)的是時(shí)間亏栈,data存儲(chǔ)的是解析后的數(shù)據(jù)台腥。
Message中常用的方法是get和set方法,get是從data中取出想要的數(shù)據(jù)绒北,set是將解析后的intent等信息存入data中黎侈。
有的時(shí)候,我們可能要傳入更多的信息進(jìn)入nlu模塊闷游,例如峻汉,在我們的智能客服場(chǎng)景下,需要傳入scene_id(場(chǎng)景id)脐往,這個(gè)時(shí)候我們需要修改源碼休吠,以支持傳入scene__id,需要修改的py文件如下:
rasa/nlu/training_data/message.py
rasa/nlu/model.py
rasa/core/interpreter.py
rasa/core/processor.py
下面將一一解析每一塊代碼的作用以及需要修改的部分业簿。
1瘤礁、message.py
message.py中定義的class Message是nlu模塊傳入的數(shù)據(jù),它有set梅尤、get等方法柜思,為了傳入其他數(shù)據(jù)進(jìn)入nlu模塊,我們需要在初始化部分初始化要傳輸?shù)臄?shù)據(jù)巷燥。
2赡盘、model.py
model.py中定義了Interpreter類,主要用于解析數(shù)據(jù)缰揪,該類在rasa/core/interpreter.py中的RasaNLUInterpreter調(diào)用陨享,用于解析text,修改其中的初始parse_data邀跃,用于傳輸額外數(shù)據(jù)給nlu模塊霉咨。
3、interpreter.py
interpreter.py中定義了數(shù)據(jù)的解析方法拍屑,在測(cè)試時(shí)途戒,Rasa采用的是RasaNLUInterpreter。所以要修改class RasaNLUInterpreter中的parse 函數(shù)僵驰。
下面以RasaNLUInterpreter源碼為例喷斋,詳解interpreter模塊如何調(diào)用nlu模型并輸入到processor模塊中唁毒。
class RasaNLUInterpreter(NaturalLanguageInterpreter):
? ? def __init__(
? ? ? ? self,
? ? ? ? model_directory: Text,
? ? ? ? config_file: Optional[Text] = None,
? ? ? ? lazy_init: bool = False,
? ? ):
? ? ? ? self.model_directory = model_directory
? ? ? ? self.lazy_init = lazy_init
? ? ? ? self.config_file = config_file
? ? ? ? if not lazy_init:
? ? ? ? ? ? self._load_interpreter()
? ? ? ? else:
? ? ? ? ? ? self.interpreter = None
? ? async def parse(
? ? ? ? self,
? ? ? ? text: Text,
? ? ? ? message_id: Optional[Text] = None,
? ? ? ? tracker: DialogueStateTracker = None,
? ? ) -> Dict[Text, Any]:
? ? ? ? """Parse a text message.
? ? ? ? Return a default value if the parsing of the text failed."""
? ? ? ? if self.lazy_init and self.interpreter is None:
? ? ? ? ? ? self._load_interpreter()
? ? ? ? result = self.interpreter.parse(text)
? ? ? ? return result
? ? def _load_interpreter(self) -> None:
? ? ? ? from rasa.nlu.model import Interpreter
? ? ? ? self.interpreter = Interpreter.load(self.model_directory)
在初始化的時(shí)候,通過(guò)self._load_interpreter()來(lái)引入rasa.nlu.model中的Interpreter模塊星爪,Interpreter有一個(gè)load方法浆西,load傳入已經(jīng)訓(xùn)練好的模型文件路徑,加載出nlu模型供interpreter調(diào)用顽腾。
如果想新建自己的interpreter類近零,需要注意兩點(diǎn):
如何傳入已經(jīng)訓(xùn)練好的nlu模型文件路徑和在rasa中引入自己的interpreter模塊,這部分可以在endpoints.yml中引入抄肖,詳見(jiàn)
額外數(shù)據(jù)的引入久信,由于parse方法中傳入了tracker,歷史信息都可以在tracker中獲取漓摩。
4裙士、processor.py
processor.py中將信息傳入interpreter.py中,這部分在_parse_message中管毙。
在processor中腿椎,def?parse_message調(diào)用interpreter方法,然后在外層封裝為def _handle_message_with_tracker如下所示:
async def _handle_message_with_tracker(
? ? ? ? self, message: UserMessage, tracker: DialogueStateTracker
? ? ) -> None:
? ? ? ? if message.parse_data:
? ? ? ? ? ? parse_data = message.parse_data
? ? ? ? else:
? ? ? ? ? ? parse_data = await self._parse_message(message, tracker)
? ? ? ? # don't ever directly mutate the tracker
? ? ? ? # - instead pass its events to log
? ? ? ? tracker.update(
? ? ? ? ? ? UserUttered(
? ? ? ? ? ? ? ? message.text,
? ? ? ? ? ? ? ? parse_data["intent"],
? ? ? ? ? ? ? ? parse_data["entities"],
? ? ? ? ? ? ? ? parse_data,
? ? ? ? ? ? ? ? input_channel=message.input_channel,
? ? ? ? ? ? ? ? message_id=message.message_id,
? ? ? ? ? ? ? ? metadata=message.metadata,
? ? ? ? ? ? ),
? ? ? ? ? ? self.domain,
? ? ? ? )
? ? ? ? if parse_data["entities"]:
? ? ? ? ? ? self._log_slots(tracker)
? ? ? ? logger.debug(
? ? ? ? ? ? f"Logged UserUtterance - tracker now has {len(tracker.events)} events."
? ? ? ? )
從def _handle_message_with_tracker可以看到夭咬,首先檢查message.parse_data是否為空(channel.py里面給parse_data傳值)啃炸,如果為空,調(diào)用interpreter去識(shí)別意圖等皱埠,最后將結(jié)果寫(xiě)入parse_data中肮帐。
二咖驮、自定義nlu組件
rasa的nlu模塊除了自身提供的實(shí)體抽取和意圖識(shí)別模型外边器,還可以添加自定義模型,如自定義意圖識(shí)別模塊或者情感分析等模塊托修。下面將詳細(xì)介紹自定義模塊的添加忘巧。
1、數(shù)據(jù)結(jié)構(gòu)
傳入rasa_nlu的數(shù)據(jù)存儲(chǔ)格式為Message對(duì)象睦刃,Message對(duì)象定義在rasa/nlu/training_data/message.py中砚嘴。其中message.text中就是輸入的文本。
2涩拙、處理流程
下圖是文本傳入rasa_nlu后的標(biāo)準(zhǔn)流程际长。
2.1 tokenizer
rasa提供了5種分詞(Tokenizers)如下:
TokenizerRequiresDescription
WhitespaceTokenizer/為每個(gè)以空格分隔的字符序列創(chuàng)建token。
MitieTokenizer需要先配置MitieNLP使用Mitie進(jìn)行分詞兴泥,用MITIE tokenizer創(chuàng)建tokens工育,從而服務(wù)于 MITIEFeaturizer。
SpacyTokenizer需要先配置SpacyNLP使用Spacy進(jìn)行分詞搓彻,用Spacytokenizer創(chuàng)建tokens如绸,從而服務(wù)于SpacyFeaturizer嘱朽。
ConveRTTokenizer/使用ConveRt進(jìn)行分詞,用ConveRT Tokenizer創(chuàng)建tokens怔接,從而服務(wù)于ConveRTFeaturizer搪泳。
JiebaTokenizer/使用Jieba作為 Tokenizer,對(duì)中文進(jìn)行分詞扼脐,用戶的自定義字典文件可以通過(guò)特定的文件目錄路徑 dictionary_path自動(dòng)加載岸军。
分詞后的結(jié)果會(huì)回傳進(jìn)Message對(duì)象中,key值為"tokens"瓦侮,詳見(jiàn)rasa/nlu/tokenizers/tokenizer.py的process方法凛膏。
2.2 featurizer
rasa提供了2類featurizer方法— dense_featurizer和sparse_featurizer。
dense_featurizer:
FeaturizerRequiresDescription
MitieFeaturizer需要先配置MitieNLP使用MITIE featurizer為意圖分類創(chuàng)建特征脏榆。
SpacyFeaturizer需要先配置SpacyNLP使用spacy featurizer為意圖分類創(chuàng)建特征猖毫。
ConveRTFeaturizer需要配置ConveRTTokenizer使用ConveRT模型創(chuàng)建用戶消息和響應(yīng),由于ConveRT模型僅在英語(yǔ)語(yǔ)料上訓(xùn)練须喂,因此只有當(dāng)訓(xùn)練數(shù)據(jù)是英語(yǔ)語(yǔ)言時(shí)才能使用這個(gè)featurizer吁断。
sparse_featurizer:
FeaturizerRequiresDescription
RegexFeaturizer/為實(shí)體提取和意圖分類創(chuàng)建特征。在訓(xùn)練期間坞生,regex intent featurizer 以訓(xùn)練數(shù)據(jù)的格式創(chuàng)建一系列正則表達(dá)式列表仔役。對(duì)于每個(gè)正則,都將設(shè)置一個(gè)特征是己,標(biāo)記是否在輸入中找到該表達(dá)式又兵,然后將其輸入到intent classifier / entity extractor 中以簡(jiǎn)化分類(假設(shè)分類器在訓(xùn)練階段已經(jīng)學(xué)習(xí)了該特征集合,該特征集合表示一定的意圖)
CountVectorsFeaturizer/創(chuàng)建用戶信息和標(biāo)簽(意圖和響應(yīng))的詞袋表征卒废,用作意圖分類器的輸入沛厨,輸入的意圖特征以詞袋表征
特征化后的結(jié)果會(huì)回傳進(jìn)Message對(duì)象中,key值為"text_dense_features"/"text_sparse_features"摔认。
2.3 intent classifier
ClassifierRequiresDescription
MitieIntentClassifiertokenizer 和 featurizer該分類器使用MITIE進(jìn)行意圖分類逆皮。底層分類器使用的是具有稀疏線性核的多類線性支持向量機(jī)
SklearnIntentClassifierfeaturizer該sklearn意圖分類器訓(xùn)練一個(gè)線性支持向量機(jī),該支持向量機(jī)通過(guò)網(wǎng)格搜索得到優(yōu)化参袱。除了其他分類器电谣,它還提供沒(méi)有“獲勝”的標(biāo)簽的排名。spacy意圖分類器需要在管道中的先加入一個(gè)featurizer抹蚀。該featurizer創(chuàng)建用于分類的特征剿牺。
EmbeddingIntentClassifierfeaturizer嵌入式意圖分類器將用戶輸入和意圖標(biāo)簽嵌入到同一空間中。Supervised embeddings通過(guò)最大化它們之間的相似性來(lái)訓(xùn)練环壤。該算法基于StarSpace的晒来。但是,在這個(gè)實(shí)現(xiàn)中镐捧,損失函數(shù)略有不同潜索,添加了額外的隱藏層和dropout臭增。該算法還提供了未“獲勝”標(biāo)簽的相似度排序。在embedding intent classifier之前竹习,需要在管道中加入一個(gè)featurizer誊抛。該featurizer創(chuàng)建用以embeddings的特征。建議使用CountVectorsFeaturizer整陌,它可選的預(yù)處理有SpacyNLP和SpacyTokenizer拗窃。
KeywordIntentClassifier/該分類器通過(guò)搜索關(guān)鍵字的消息來(lái)工作。默認(rèn)情況下泌辫,匹配是大小寫(xiě)敏感的随夸,只精確匹配地搜索用戶消息中關(guān)鍵字。
classifier會(huì)從Message對(duì)象中獲取tokenizer 和 featurizer作為分類模型的輸入進(jìn)行意圖識(shí)別震放。
3宾毒、自定義方法
由2中的處理流程可以看出,rasa提供的意圖識(shí)別重點(diǎn)是featurizer殿遂,rasa本身提供了5種featurizer方法诈铛,除了ConveRT模型之外均可用與中文場(chǎng)景,其中MitieFeaturizer和SpacyFeaturizer需要特定的Tokenizer和對(duì)應(yīng)的詞向量墨礁。
我們也可以自定義自己的中文模型幢竹,先通過(guò)jieba分詞(可以引入用戶字典),然后用自己訓(xùn)練的詞向量模型來(lái)featurize用戶輸入的text恩静,最后再引入自定義的分類模型識(shí)別意圖焕毫。
以KeywordIntentClassifier意圖識(shí)別模型為例,解釋如何自定義意圖識(shí)別模型:
class KeywordIntentClassifier(Component):
? ? """Intent classifier using simple keyword matching.
? ? The classifier takes a list of keywords and associated intents as an input.
? ? A input sentence is checked for the keywords and the intent is returned.
? ? """
? ? provides = [INTENT_ATTRIBUTE]
? ? defaults = {"case_sensitive": True}
? ? def __init__(
? ? ? ? self,
? ? ? ? component_config: Optional[Dict[Text, Any]] = None,
? ? ? ? intent_keyword_map: Optional[Dict] = None,
? ? ):
? ? ? ? super(KeywordIntentClassifier, self).__init__(component_config)
? ? ? ? self.case_sensitive = self.component_config.get("case_sensitive")
? ? ? ? self.intent_keyword_map = intent_keyword_map or {}
? ? def train(
? ? ? ? self,
? ? ? ? training_data: "TrainingData",
? ? ? ? cfg: Optional["RasaNLUModelConfig"] = None,
? ? ? ? **kwargs: Any,
? ? ) -> None:
? ? ? ? duplicate_examples = set()
? ? ? ? for ex in training_data.training_examples:
? ? ? ? ? ? if (
? ? ? ? ? ? ? ? ex.text in self.intent_keyword_map.keys()
? ? ? ? ? ? ? ? and ex.get(INTENT_ATTRIBUTE) != self.intent_keyword_map[ex.text]
? ? ? ? ? ? ):
? ? ? ? ? ? ? ? duplicate_examples.add(ex.text)
? ? ? ? ? ? ? ? raise_warning(
? ? ? ? ? ? ? ? ? ? f"Keyword '{ex.text}' is a keyword to trigger intent "
? ? ? ? ? ? ? ? ? ? f"'{self.intent_keyword_map[ex.text]}' and also "
? ? ? ? ? ? ? ? ? ? f"intent '{ex.get(INTENT_ATTRIBUTE)}', it will be removed "
? ? ? ? ? ? ? ? ? ? f"from the list of keywords for both of them. "
? ? ? ? ? ? ? ? ? ? f"Remove (one of) the duplicates from the training data.",
? ? ? ? ? ? ? ? ? ? docs=DOCS_URL_COMPONENTS + "#keyword-intent-classifier",
? ? ? ? ? ? ? ? )
? ? ? ? ? ? else:
? ? ? ? ? ? ? ? self.intent_keyword_map[ex.text] = ex.get(INTENT_ATTRIBUTE)
? ? ? ? for keyword in duplicate_examples:
? ? ? ? ? ? self.intent_keyword_map.pop(keyword)
? ? ? ? ? ? logger.debug(
? ? ? ? ? ? ? ? f"Removed '{keyword}' from the list of keywords because it was "
? ? ? ? ? ? ? ? "a keyword for more than one intent."
? ? ? ? ? ? )
? ? ? ? self._validate_keyword_map()
? ? def _validate_keyword_map(self) -> None:
? ? ? ? re_flag = 0 if self.case_sensitive else re.IGNORECASE
? ? ? ? ambiguous_mappings = []
? ? ? ? for keyword1, intent1 in self.intent_keyword_map.items():
? ? ? ? ? ? for keyword2, intent2 in self.intent_keyword_map.items():
? ? ? ? ? ? ? ? if (
? ? ? ? ? ? ? ? ? ? re.search(r"\b" + keyword1 + r"\b", keyword2, flags=re_flag)
? ? ? ? ? ? ? ? ? ? and intent1 != intent2
? ? ? ? ? ? ? ? ):
? ? ? ? ? ? ? ? ? ? ambiguous_mappings.append((intent1, keyword1))
? ? ? ? ? ? ? ? ? ? raise_warning(
? ? ? ? ? ? ? ? ? ? ? ? f"Keyword '{keyword1}' is a keyword of intent '{intent1}', "
? ? ? ? ? ? ? ? ? ? ? ? f"but also a substring of '{keyword2}', which is a "
? ? ? ? ? ? ? ? ? ? ? ? f"keyword of intent '{intent2}."
? ? ? ? ? ? ? ? ? ? ? ? f" '{keyword1}' will be removed from the list of keywords.\n"
? ? ? ? ? ? ? ? ? ? ? ? f"Remove (one of) the conflicting keywords from the"
? ? ? ? ? ? ? ? ? ? ? ? f" training data.",
? ? ? ? ? ? ? ? ? ? ? ? docs=DOCS_URL_COMPONENTS + "#keyword-intent-classifier",
? ? ? ? ? ? ? ? ? ? )
? ? ? ? for intent, keyword in ambiguous_mappings:
? ? ? ? ? ? self.intent_keyword_map.pop(keyword)
? ? ? ? ? ? logger.debug(
? ? ? ? ? ? ? ? f"Removed keyword '{keyword}' from intent '{intent}' because it matched a "
? ? ? ? ? ? ? ? "keyword of another intent."
? ? ? ? ? ? )
? ? def process(self, message: Message, **kwargs: Any) -> None:
? ? ? ? intent_name = self._map_keyword_to_intent(message.text)
? ? ? ? confidence = 0.0 if intent_name is None else 1.0
? ? ? ? intent = {"name": intent_name, "confidence": confidence}
? ? ? ? if message.get(INTENT_ATTRIBUTE) is None or intent is not None:
? ? ? ? ? ? message.set(INTENT_ATTRIBUTE, intent, add_to_output=True)
? ? def _map_keyword_to_intent(self, text: Text) -> Optional[Text]:
? ? ? ? re_flag = 0 if self.case_sensitive else re.IGNORECASE
? ? ? ? for keyword, intent in self.intent_keyword_map.items():
? ? ? ? ? ? if re.search(r"\b" + keyword + r"\b", text, flags=re_flag):
? ? ? ? ? ? ? ? logger.debug(
? ? ? ? ? ? ? ? ? ? f"KeywordClassifier matched keyword '{keyword}' to"
? ? ? ? ? ? ? ? ? ? f" intent '{intent}'."
? ? ? ? ? ? ? ? )
? ? ? ? ? ? ? ? return intent
? ? ? ? logger.debug("KeywordClassifier did not find any keywords in the message.")
? ? ? ? return None
? ? def persist(self, file_name: Text, model_dir: Text) -> Dict[Text, Any]:
? ? ? ? """Persist this model into the passed directory.
? ? ? ? Return the metadata necessary to load the model again.
? ? ? ? """
? ? ? ? file_name = file_name + ".json"
? ? ? ? keyword_file = os.path.join(model_dir, file_name)
? ? ? ? utils.write_json_to_file(keyword_file, self.intent_keyword_map)
? ? ? ? return {"file": file_name}
? ? @classmethod
? ? def load(
? ? ? ? cls,
? ? ? ? meta: Dict[Text, Any],
? ? ? ? model_dir: Optional[Text] = None,
? ? ? ? model_metadata: "Metadata" = None,
? ? ? ? cached_component: Optional["KeywordIntentClassifier"] = None,
? ? ? ? **kwargs: Any,
? ? ) -> "KeywordIntentClassifier":
? ? ? ? if model_dir and meta.get("file"):
? ? ? ? ? ? file_name = meta.get("file")
? ? ? ? ? ? keyword_file = os.path.join(model_dir, file_name)
? ? ? ? ? ? if os.path.exists(keyword_file):
? ? ? ? ? ? ? ? intent_keyword_map = utils.read_json_file(keyword_file)
? ? ? ? ? ? else:
? ? ? ? ? ? ? ? raise_warning(
? ? ? ? ? ? ? ? ? ? f"Failed to load key word file for `IntentKeywordClassifier`, "
? ? ? ? ? ? ? ? ? ? f"maybe {keyword_file} does not exist?",
? ? ? ? ? ? ? ? )
? ? ? ? ? ? ? ? intent_keyword_map = None
? ? ? ? ? ? return cls(meta, intent_keyword_map)
? ? ? ? else:
? ? ? ? ? ? raise Exception(
? ? ? ? ? ? ? ? f"Failed to load keyword intent classifier model. "
? ? ? ? ? ? ? ? f"Path {os.path.abspath(meta.get('file'))} doesn't exist."
? ? ? ? ? ? )
從上面的代碼可以看到驶乾,整個(gè)KeywordIntentClassifier由4部分組成邑飒,train、process轻掩、persist和load幸乒。其中懦底,train是模型訓(xùn)練部分唇牧,process是模型推理部分,persist是模型持久化部分聚唐,load是加載訓(xùn)練好的模型丐重。一開(kāi)始的provide定義的是返回值提供什么字段,default是模型的一些自定義參數(shù)杆查,比如模型的epochs和batch_size等扮惦,這里定義的是這些參數(shù)的默認(rèn)值,在config.yml中自定義的pipeline可以傳入其他值亲桦,如下所示: