Rasa中使用lookup table時針對中文對RegexEntityExtractor進行修改

SoCalledHBY發表於2020-11-17

環境:Python 3.7.9
   Rasa 2.0.6
   Rasa SDK 2.0.0

一、問題

  博主在使用Rasa做中文問答時遇到了一個問題:新增form,slot filling使用from_entity,並在pipeline中新增RegexEntityExtractor。假設該entity為city,在nlu.yml中僅新增了鄭州作為training data,且在nlu.yml中新增了city的lookup table,如圖。

nlu.yml
  但在實際對話中,除了鄭州可以被DIETClassifier識別到,lookup table中沒有出現在training data中的的例子均無法正常auto fill,如圖。

rasa shell nlu

二、分析

  為什麼RegexEntityExtractor無法識別lookup table中的例子呢?在官方文件查詢無果,於是果斷從原始碼入手,分析RegexEntityExtractor

# regex_entity_extractor.py

import rasa.nlu.utils.pattern_utils as pattern_utils
...

class RegexEntityExtractor(EntityExtractor):
    """Searches for entities in the user's message using the lookup tables and regexes
    defined in the training data."""

	...

    def train(
        self,
        training_data: TrainingData,
        config: Optional[RasaNLUModelConfig] = None,
        **kwargs: Any,
    ) -> None:
        self.patterns = pattern_utils.extract_patterns(
            training_data,
            use_lookup_tables=self.component_config["use_lookup_tables"],
            use_regexes=self.component_config["use_regexes"],
            use_only_entities=True,
        )

        if not self.patterns:
            rasa.shared.utils.io.raise_warning(
                "No lookup tables or regexes defined in the training data that have "
                "a name equal to any entity in the training data. In order for this "
                "component to work you need to define valid lookup tables or regexes "
                "in the training data."
            )

	...

    def _extract_entities(self, message: Message) -> List[Dict[Text, Any]]:
        """Extract entities of the given type from the given user message."""

		...

        for pattern in self.patterns:
            matches = re.finditer(pattern["pattern"], message.get(TEXT), flags=flags)
            matches = list(matches)

            for match in matches:
                start_index = match.start()
                end_index = match.end()
                entities.append(
                    {
                        ENTITY_ATTRIBUTE_TYPE: pattern["name"],
                        ENTITY_ATTRIBUTE_START: start_index,
                        ENTITY_ATTRIBUTE_END: end_index,
                        ENTITY_ATTRIBUTE_VALUE: message.get(TEXT)[
                            start_index:end_index
                        ],
                    }
                )

        return entities
	...

  這裡只列出兩個最重要的方法。
  可以看到,在_extract_entities方法中RegexEntityExtractor使用了self.patterns中的正規表示式對使用者的輸入進行匹配,在train方法中可以看到,self.patterns是通過呼叫了pattern_utilsextract_patterns方法得到的,於是繼續追蹤。

# pattern_utils.py

def _convert_lookup_tables_to_regex(
    training_data: TrainingData, use_only_entities: bool = False
) -> List[Dict[Text, Text]]:
    """Convert the lookup tables from the training data to regex patterns.
    Args:
        training_data: The training data.
        use_only_entities: If True only regex features with a name equal to a entity
          are considered.

    Returns:
        A list of regex patterns.
    """
    patterns = []
    for table in training_data.lookup_tables:
        if use_only_entities and table["name"] not in training_data.entities:
            continue
        regex_pattern = _generate_lookup_regex(table)
        lookup_regex = {"name": table["name"], "pattern": regex_pattern}
        patterns.append(lookup_regex)
    return patterns


def _generate_lookup_regex(lookup_table: Dict[Text, Union[Text, List[Text]]]) -> Text:
    """Creates a regex pattern from the given lookup table.

    The lookup table is either a file or a list of entries.

    Args:
        lookup_table: The lookup table.

    Returns:
        The regex pattern.
    """
    lookup_elements = lookup_table["elements"]

    # if it's a list, it should be the elements directly
    if isinstance(lookup_elements, list):
        elements_to_regex = lookup_elements
    # otherwise it's a file path.
    else:
        elements_to_regex = read_lookup_table_file(lookup_elements)

    # sanitize the regex, escape special characters
    elements_sanitized = [re.escape(e) for e in elements_to_regex]

    # regex matching elements with word boundaries on either side
    return "(\\b" + "\\b|\\b".join(elements_sanitized) + "\\b)"

...

def extract_patterns(
    training_data: TrainingData,
    use_lookup_tables: bool = True,
    use_regexes: bool = True,
    use_only_entities: bool = False,
) -> List[Dict[Text, Text]]:
    """Extract a list of patterns from the training data.

    The patterns are constructed using the regex features and lookup tables defined
    in the training data.

    Args:
        training_data: The training data.
        use_only_entities: If True only lookup tables and regex features with a name
          equal to a entity are considered.
        use_regexes: Boolean indicating whether to use regex features or not.
        use_lookup_tables: Boolean indicating whether to use lookup tables or not.

    Returns:
        The list of regex patterns.
    """
    if not training_data.lookup_tables and not training_data.regex_features:
        return []

    patterns = []

    if use_regexes:
        patterns.extend(_collect_regex_features(training_data, use_only_entities))
    if use_lookup_tables:
        patterns.extend(
            _convert_lookup_tables_to_regex(training_data, use_only_entities)
        )

    return patterns

  這裡只列出三個最重要的方法。
  可以看到,在extract_patterns方法中會判斷使用者是否開啟了use_lookup_tables選項,如果啟用,則呼叫_convert_lookup_tables_to_regex方法,即將lookup table轉換為regex。在官方文件中,我們也可以看到,查詢表是需要轉換為正規表示式進行匹配的:

Lookup tables are lists of words used to generate case-insensitive regular expression patterns.
查詢表是用來生成大小寫敏感的正規表示式的單詞的列表。

  繼續追蹤,在_convert_lookup_tables_to_regex方法中可以看到,正規表示式又是呼叫_generate_lookup_regex方法生成的。最終,我們來到了_generate_lookup_regex方法,發現了事情的真相。直接看return的部分,我們發現,返回的正規表示式並不是簡單地將查詢表中的例子用|連線起來,而是在每個例子前後都加上了一個\b,而這個\b就是問題的關鍵。經過搜尋(博主並不擅長正規表示式,見諒),原來\b是為了在匹配時只匹配邊界的例子,如er\b可以匹配never中的er,但不能匹配verb中的er,而中文的單詞間並沒有空格,導致句子中的例子無法被識別。

三、解決

  真相大白,只需將rasa/nlu/utils/pattern_utils/pattern_utils.py_generate_lookup_regex方法中的返回值中的\\b刪去,即可得到適合中文的RegexEntityExtractor

四、解決?

  正當博主以為真相已經水落石出之時,卻偶然發現,北京、上海等大城市並沒有出現該問題,可以被DIETClassifier正常auto fill,甚至不新增至lookup table也可以識別…目測可能和博主使用的預訓練模型有關,有待進一步求證。

相關文章