From 6ca7b66fb4f4ab4edd704738e3a2bcf9bdf1c2a3 Mon Sep 17 00:00:00 2001 From: kb Date: Wed, 22 May 2024 23:45:33 +0200 Subject: [PATCH] Finishing up NLU module --- .gitignore | 4 + README.md | 11 +- evaluate.py | 3 +- nlu_train.py | 4 +- requirements.txt | 5 + src/__init__.py | 0 src/main.py | 48 +++-- src/model/__init__.py | 0 src/model/frame.py | 9 +- src/model/slot.py | 5 +- nlu_tests.py => src/nlu_example.py | 58 +++--- src/service/__init__.py | 0 src/service/dialog_policy.py | 7 +- src/service/natural_languag_understanding.py | 100 +++++++++- src/service/natural_language_generation.py | 2 +- src/utils/__init__.py | 0 nlu_utils.py => src/utils/nlu_utils.py | 199 +++++++++---------- 17 files changed, 284 insertions(+), 171 deletions(-) create mode 100644 .gitignore create mode 100644 requirements.txt create mode 100644 src/__init__.py create mode 100644 src/model/__init__.py rename nlu_tests.py => src/nlu_example.py (73%) create mode 100644 src/service/__init__.py create mode 100644 src/utils/__init__.py rename nlu_utils.py => src/utils/nlu_utils.py (96%) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a438516 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__ +slot-model* +frame-model* +nlu_cache \ No newline at end of file diff --git a/README.md b/README.md index 854f385..a2f7fcc 100644 --- a/README.md +++ b/README.md @@ -34,4 +34,13 @@ Agent powinien wykazywać elastyczność, adaptując się do potrzeb klienta, np | inform | poinformowanie użytkownika, o przyjętej wartości slotu | | offer | rekomendacja (restauracji) | | request | pytanie użytkownika o wartość slotu | -| select | prośba o dokonanie wyboru spośród przedstawionych opcji | \ No newline at end of file +| select | prośba o dokonanie wyboru spośród przedstawionych opcji | + +# Obsługa projektu + +- Python 3.10.12 +- Instalacja dependencies `pip3 install -r requirements.txt` +- Centralna część systemu - uruchamiamy `python3 src/main.py` +- NLU: + - uczenie modeli od zera `python3 nlu_train.py` + - Ewaluacja `python3 evaluate.py` diff --git a/evaluate.py b/evaluate.py index 989fa18..c86f41b 100644 --- a/evaluate.py +++ b/evaluate.py @@ -2,11 +2,10 @@ import re import os import pandas as pd import numpy as np -from nlu_utils import predict_multiple from flair.models import SequenceTagger from conllu import parse_incr from flair.data import Corpus -from nlu_utils import conllu2flair, nolabel2o +from src.utils.nlu_utils import conllu2flair, nolabel2o, predict_multiple # Frame model evaluation frame_model = SequenceTagger.load('frame-model-prod/best-model.pt') diff --git a/nlu_train.py b/nlu_train.py index acbca67..b3af91b 100644 --- a/nlu_train.py +++ b/nlu_train.py @@ -6,7 +6,7 @@ from flair.embeddings import CharacterEmbeddings from flair.embeddings import FlairEmbeddings from flair.models import SequenceTagger from flair.trainers import ModelTrainer -from nlu_utils import conllu2flair, nolabel2o +from src.utils.nlu_utils import conllu2flair, nolabel2o import torch if torch.cuda.is_available(): @@ -38,4 +38,4 @@ def train_model(label_type, field_parsers = {}): if __name__ == '__main__': train_model("frame") - train_model('slot', field_parsers={'slot': nolabel2o}) \ No newline at end of file + train_model('slot', field_parsers={'slot': nolabel2o}) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..565e922 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +flair==0.13.1 +conllu==4.5.3 +pandas==1.5.3 +numpy==1.26.4 +torch==2.3.0 \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/main.py b/src/main.py index 5eadf5b..a29b0c8 100644 --- a/src/main.py +++ b/src/main.py @@ -1,26 +1,38 @@ -from model.frame import Frame from service.dialog_state_monitor import DialogStateMonitor from service.dialog_policy import DialogPolicy from service.natural_languag_understanding import NaturalLanguageUnderstanding from service.natural_language_generation import NaturalLanguageGeneration -print("Natural language understanding, example:") -naturalLanguageUnderstanding = NaturalLanguageUnderstanding() -print(naturalLanguageUnderstanding.convert_text_to_frame("Cześć, jak masz na imię?")) +# initialize classes +nlu = NaturalLanguageUnderstanding() # NLU +monitor = DialogStateMonitor() # DSM +dialog_policy = DialogPolicy() # DP +language_generation = NaturalLanguageGeneration() # NLG + +# Main loop +user_input = input("Możesz zacząć pisać.\n") +while True: + # NLU + frame = nlu.process_input(user_input) + print(frame) + + # DSM + monitor.append(frame) + + # DP + print(dialog_policy.next_dialogue_act(monitor.get_all()).act) + + # NLG + response = language_generation.respond_to_name_query("Jak masz na imię?") + print(response) + + if frame.act == "bye": + break + + user_input = input(">\n") + + + -# Example -print("Dialog state monitor, examples:") -monitor = DialogStateMonitor() -monitor.append(Frame('system', 'hello', [])) -monitor.append(Frame('user', 'some_text', [])) -print(monitor.get_all()[0].act) -print(monitor.get_last().act) -print("Dialog policy, next dialogue act:") -dialog_policy = DialogPolicy(monitor.get_all()) -print(dialog_policy.next_dialogue_act().act) -print("Natural Language Generation example:") -agent = NaturalLanguageGeneration() -response = agent.respond_to_name_query("Jak masz na imię?") -print(response) diff --git a/src/model/__init__.py b/src/model/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/model/frame.py b/src/model/frame.py index a4ae6d5..01a9c71 100644 --- a/src/model/frame.py +++ b/src/model/frame.py @@ -1,7 +1,14 @@ -from model.slot import Slot +from .slot import Slot class Frame: def __init__(self, source: str, act: str, slots: list[Slot]): self.source = source self.slots = slots self.act = act + + def __str__(self): + msg = f"Act: {self.act}, Slots: [" + for slot in self.slots: + msg += f"({slot}), " + msg += "]" + return msg diff --git a/src/model/slot.py b/src/model/slot.py index df4956f..6ad0e6f 100644 --- a/src/model/slot.py +++ b/src/model/slot.py @@ -1,4 +1,7 @@ class Slot: def __init__(self, name, value=None): self.name = name - self.value = value \ No newline at end of file + self.value = value + + def __str__(self) -> str: + return f"Name: {self.name}, Value: {self.value}" \ No newline at end of file diff --git a/nlu_tests.py b/src/nlu_example.py similarity index 73% rename from nlu_tests.py rename to src/nlu_example.py index 98b1c8e..265f05c 100644 --- a/nlu_tests.py +++ b/src/nlu_example.py @@ -1,30 +1,30 @@ -from flair.models import SequenceTagger -from nlu_utils import predict_single, predict_multiple, predict_and_annotate - -# Exploratory tests -frame_model = SequenceTagger.load('frame-model/best-model.pt') -tests = [ - 'chciałbym zamówić pizzę', - 'na godzinę 12', - 'prosiłbym o pizzę z pieczarkami', - 'to wszystko, jaka cena?', - 'ile kosztuje pizza', - 'do widzenia', - 'tak', - 'nie dziękuję', - 'dodatkowy ser', - 'pizzę barcelona bez cebuli', -] - -# print("=== Exploratory tests - frame model ===") -for test in tests: - print(f"Sentence: {test}") - print(f"Single prediction: {predict_single(frame_model, test.split(), 'frame')}") - print(f"Multiple predictions: {predict_multiple(frame_model, test.split(), 'frame')}") - print(f"Annotated sentence: {predict_and_annotate(frame_model, test.split(), 'frame')}") - -print("=== Exploratory tests - slot model ===") -slot_model = SequenceTagger.load('slot-model/final-model.pt') -for test in tests: - print(f"Sentence: {test}") +from flair.models import SequenceTagger +from utils.nlu_utils import predict_single, predict_multiple, predict_and_annotate + +# Exploratory tests +frame_model = SequenceTagger.load('frame-model-prod/best-model.pt') +tests = [ + 'chciałbym zamówić pizzę', + 'na godzinę 12', + 'prosiłbym o pizzę z pieczarkami', + 'to wszystko, jaka cena?', + 'ile kosztuje pizza', + 'do widzenia', + 'tak', + 'nie dziękuję', + 'dodatkowy ser', + 'pizzę barcelona bez cebuli', +] + +print("=== Exploratory tests - frame model ===") +for test in tests: + print(f"Sentence: {test}") + print(f"Single prediction: {predict_single(frame_model, test.split(), 'frame')}") + print(f"Multiple predictions: {predict_multiple(frame_model, test.split(), 'frame')}") + print(f"Annotated sentence: {predict_and_annotate(frame_model, test.split(), 'frame')}") + +print("=== Exploratory tests - slot model ===") +slot_model = SequenceTagger.load('slot-model-prod/best-model.pt') +for test in tests: + print(f"Sentence: {test}") print(f"Prediction: {predict_and_annotate(slot_model, test.split(), 'slot')}") \ No newline at end of file diff --git a/src/service/__init__.py b/src/service/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/service/dialog_policy.py b/src/service/dialog_policy.py index 28fe96e..ee9d29c 100644 --- a/src/service/dialog_policy.py +++ b/src/service/dialog_policy.py @@ -1,11 +1,8 @@ from model.frame import Frame class DialogPolicy: - def __init__(self, frames: list[Frame]) -> None: - self.frames = frames - - def next_dialogue_act(self) -> Frame: - if self.frames[-1].act == "welcomemsg": + def next_dialogue_act(self, frames: list[Frame]) -> Frame: + if frames[-1].act == "welcomemsg": return Frame("system", "welcomemsg", []) else: return Frame("system", "canthelp", []) diff --git a/src/service/natural_languag_understanding.py b/src/service/natural_languag_understanding.py index 628c208..394ff55 100644 --- a/src/service/natural_languag_understanding.py +++ b/src/service/natural_languag_understanding.py @@ -1,14 +1,92 @@ +from flair.models import SequenceTagger +from utils.nlu_utils import predict_single, predict_and_annotate +from model.frame import Frame, Slot + +""" +ACTS: + inform/order + request/menu + inform/address + request/price + request/ingredients + request/sauce + inform/phone + inform/order-complete + request/time + request/size + welcomemsg + affirm + inform/delivery + inform/payment + request/delivery-price + bye + inform/time + request/drinks + inform/name + negate + +SLOTS: + food + pizza + size + address + quantity + ingredient + payment-method + delivery + drink + ingredient/neg + name + phone + sauce +""" + class NaturalLanguageUnderstanding: + def __init__(self): + print("\n========================================================") + print("Models are loading, it may take a moment, please wait...") + print("========================================================\n") - dictionary = { - "Cześć," : "welcomemsg()", - "imię?" : "request(name)" - } + self.frame_model = SequenceTagger.load('frame-model-prod/best-model.pt') + self.slot_model = SequenceTagger.load('slot-model-prod/best-model.pt') - def convert_text_to_frame(self, text: str): - frame = "" - text = text.split(" ") - for word in text: - if(word in self.dictionary): - frame+=self.dictionary[word]+"&" - return frame[0:-1] + print("\n========================================================") + print("Models loaded. NLU system is ready.") + print("========================================================\n") + + def __predict_intention(self, text: str): + return predict_single(self.frame_model, text.split(), 'frame') + + def __predict_slot(self, text: str): + anootations = predict_and_annotate(self.slot_model, text.split(), 'slot') + current_slot = None + current_slot_value = "" + slots = [] + + for annotation in anootations: + form = annotation["form"] + slot = annotation["slot"] + + if slot[0:2] == "B-": + if current_slot != None: + slots.append(Slot(name=current_slot, value=current_slot_value)) + current_slot = slot[2:] + current_slot_value = form + elif slot[0:2] == "I-": + current_slot_value = current_slot_value + " " + form + elif slot == "O": + if current_slot != None: + slots.append(Slot(name=current_slot, value=current_slot_value)) + current_slot = None + current_slot_value = "" + + if current_slot != None: + slots.append(Slot(name=current_slot, value=current_slot_value)) + + return slots + + def process_input(self, text: str): + act = self.__predict_intention(text) + slots = self.__predict_slot(text) + frame = Frame(source = 'user', act = act, slots = slots) + return frame \ No newline at end of file diff --git a/src/service/natural_language_generation.py b/src/service/natural_language_generation.py index efbccf1..361bd25 100644 --- a/src/service/natural_language_generation.py +++ b/src/service/natural_language_generation.py @@ -1,5 +1,5 @@ class NaturalLanguageGeneration: - def __init__(self, name): + def __init__(self): self.name = ["Michał"] def respond_to_name_query(self, question): diff --git a/src/utils/__init__.py b/src/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nlu_utils.py b/src/utils/nlu_utils.py similarity index 96% rename from nlu_utils.py rename to src/utils/nlu_utils.py index b8166a5..55c526a 100644 --- a/nlu_utils.py +++ b/src/utils/nlu_utils.py @@ -1,101 +1,100 @@ -from flair.data import Sentence -from flair.datasets import FlairDatapointDataset - -def nolabel2o(line, i): - return 'O' if line[i] == 'NoLabel' else line[i] - -def conllu2flair(sentences, label=None): - if label == "frame": - return conllu2flair_frame(sentences, label) - else: - return conllu2flair_slot(sentences, label) - -def conllu2flair_frame(sentences, label=None): - fsentences = [] - for sentence in sentences: - tokens = [token["form"] for token in sentence] - fsentence = Sentence(' '.join(tokens), use_tokenizer=False) - - for i in range(len(fsentence)): - fsentence[i:i+1].add_label(label, sentence[i][label]) - - fsentences.append(fsentence) - - return FlairDatapointDataset(fsentences) - -def conllu2flair_slot(sentences, label=None): - fsentences = [] - for sentence in sentences: - fsentence = Sentence(' '.join(token['form'] for token in sentence), use_tokenizer=False) - start_idx = None - end_idx = None - tag = None - - if label: - for idx, (token, ftoken) in enumerate(zip(sentence, fsentence)): - if token[label].startswith('B-'): - if start_idx is not None: - fsentence[start_idx:end_idx+1].add_label(label, tag) - start_idx = idx - end_idx = idx - tag = token[label][2:] - elif token[label].startswith('I-'): - end_idx = idx - elif token[label] == 'O': - if start_idx is not None: - fsentence[start_idx:end_idx+1].add_label(label, tag) - start_idx = None - end_idx = None - tag = None - - if start_idx is not None: - fsentence[start_idx:end_idx+1].add_label(label, tag) - - fsentences.append(fsentence) - return FlairDatapointDataset(fsentences) - -def __predict(model, csentence): - fsentence = conllu2flair([csentence])[0] - model.predict(fsentence) - return fsentence - -def __csentence(sentence, label_type): - if label_type == "frame": - return [{'form': word } for word in sentence] - else: - return [{'form': word, 'slot': 'O'} for word in sentence] - -def predict_single(model, sentence, label_type): - csentence = __csentence(sentence, label_type) - fsentence = __predict(model, csentence) - intent = {} - - for span in fsentence.get_spans(label_type): - tag = span.get_label(label_type).value - if tag in intent: - intent[tag] += 1 - else: - intent[tag] = 1 - - return max(intent, key=intent.get) - -def predict_multiple(model, sentence, label_type): - csentence = __csentence(sentence, label_type) - fsentence = __predict(model, csentence) - - return set(span.get_label(label_type).value for span in fsentence.get_spans(label_type)) - -def predict_and_annotate(model, sentence, label_type): - csentence = __csentence(sentence, label_type) - fsentence = __predict(model, csentence) - - for span in fsentence.get_spans(label_type): - tag = span.get_label(label_type).value - if label_type == "frame": - csentence[span.tokens[0].idx-1]['frame'] = tag - else: - csentence[span.tokens[0].idx - 1]['slot'] = f'B-{tag}' - for token in span.tokens[1:]: - csentence[token.idx - 1]['slot'] = f'I-{tag}' - +from flair.data import Sentence +from flair.datasets import FlairDatapointDataset + +def nolabel2o(line, i): + return 'O' if line[i] == 'NoLabel' else line[i] + +def conllu2flair(sentences, label=None): + if label == "frame": + return conllu2flair_frame(sentences, label) + else: + return conllu2flair_slot(sentences, label) + +def conllu2flair_frame(sentences, label=None): + fsentences = [] + for sentence in sentences: + tokens = [token["form"] for token in sentence] + fsentence = Sentence(' '.join(tokens), use_tokenizer=False) + + for i in range(len(fsentence)): + fsentence[i:i+1].add_label(label, sentence[i][label]) + + fsentences.append(fsentence) + + return FlairDatapointDataset(fsentences) + +def conllu2flair_slot(sentences, label=None): + fsentences = [] + for sentence in sentences: + fsentence = Sentence(' '.join(token['form'] for token in sentence), use_tokenizer=False) + start_idx = None + end_idx = None + tag = None + + if label: + for idx, (token, ftoken) in enumerate(zip(sentence, fsentence)): + if token[label].startswith('B-'): + if start_idx is not None: + fsentence[start_idx:end_idx+1].add_label(label, tag) + start_idx = idx + end_idx = idx + tag = token[label][2:] + elif token[label].startswith('I-'): + end_idx = idx + elif token[label] == 'O': + if start_idx is not None: + fsentence[start_idx:end_idx+1].add_label(label, tag) + start_idx = None + end_idx = None + tag = None + + if start_idx is not None: + fsentence[start_idx:end_idx+1].add_label(label, tag) + + fsentences.append(fsentence) + return FlairDatapointDataset(fsentences) + +def __predict(model, csentence): + fsentence = conllu2flair([csentence])[0] + model.predict(fsentence) + return fsentence + +def __csentence(sentence, label_type): + if label_type == "frame": + return [{'form': word } for word in sentence] + else: + return [{'form': word, 'slot': 'O'} for word in sentence] + +def predict_single(model, sentence, label_type): + csentence = __csentence(sentence, label_type) + fsentence = __predict(model, csentence) + intent = {} + for span in fsentence.get_spans(label_type): + tag = span.get_label(label_type).value + if tag in intent: + intent[tag] += 1 + else: + intent[tag] = 1 + + return max(intent, key=intent.get) + +def predict_multiple(model, sentence, label_type): + csentence = __csentence(sentence, label_type) + fsentence = __predict(model, csentence) + + return set(span.get_label(label_type).value for span in fsentence.get_spans(label_type)) + +def predict_and_annotate(model, sentence, label_type): + csentence = __csentence(sentence, label_type) + fsentence = __predict(model, csentence) + + for span in fsentence.get_spans(label_type): + tag = span.get_label(label_type).value + if label_type == "frame": + csentence[span.tokens[0].idx-1]['frame'] = tag + else: + csentence[span.tokens[0].idx - 1]['slot'] = f'B-{tag}' + for token in span.tokens[1:]: + csentence[token.idx - 1]['slot'] = f'I-{tag}' + return csentence \ No newline at end of file