Finishing up NLU module

This commit is contained in:
s495727 2024-05-22 23:45:33 +02:00
parent f0c7b481d1
commit 6ca7b66fb4
17 changed files with 284 additions and 171 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
__pycache__
slot-model*
frame-model*
nlu_cache

View File

@ -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 | | inform | poinformowanie użytkownika, o przyjętej wartości slotu |
| offer | rekomendacja (restauracji) | | offer | rekomendacja (restauracji) |
| request | pytanie użytkownika o wartość slotu | | request | pytanie użytkownika o wartość slotu |
| select | prośba o dokonanie wyboru spośród przedstawionych opcji | | 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`

View File

@ -2,11 +2,10 @@ import re
import os import os
import pandas as pd import pandas as pd
import numpy as np import numpy as np
from nlu_utils import predict_multiple
from flair.models import SequenceTagger from flair.models import SequenceTagger
from conllu import parse_incr from conllu import parse_incr
from flair.data import Corpus 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 evaluation
frame_model = SequenceTagger.load('frame-model-prod/best-model.pt') frame_model = SequenceTagger.load('frame-model-prod/best-model.pt')

View File

@ -6,7 +6,7 @@ from flair.embeddings import CharacterEmbeddings
from flair.embeddings import FlairEmbeddings from flair.embeddings import FlairEmbeddings
from flair.models import SequenceTagger from flair.models import SequenceTagger
from flair.trainers import ModelTrainer from flair.trainers import ModelTrainer
from nlu_utils import conllu2flair, nolabel2o from src.utils.nlu_utils import conllu2flair, nolabel2o
import torch import torch
if torch.cuda.is_available(): if torch.cuda.is_available():
@ -38,4 +38,4 @@ def train_model(label_type, field_parsers = {}):
if __name__ == '__main__': if __name__ == '__main__':
train_model("frame") train_model("frame")
train_model('slot', field_parsers={'slot': nolabel2o}) train_model('slot', field_parsers={'slot': nolabel2o})

5
requirements.txt Normal file
View File

@ -0,0 +1,5 @@
flair==0.13.1
conllu==4.5.3
pandas==1.5.3
numpy==1.26.4
torch==2.3.0

0
src/__init__.py Normal file
View File

View File

@ -1,26 +1,38 @@
from model.frame import Frame
from service.dialog_state_monitor import DialogStateMonitor from service.dialog_state_monitor import DialogStateMonitor
from service.dialog_policy import DialogPolicy from service.dialog_policy import DialogPolicy
from service.natural_languag_understanding import NaturalLanguageUnderstanding from service.natural_languag_understanding import NaturalLanguageUnderstanding
from service.natural_language_generation import NaturalLanguageGeneration from service.natural_language_generation import NaturalLanguageGeneration
print("Natural language understanding, example:") # initialize classes
naturalLanguageUnderstanding = NaturalLanguageUnderstanding() nlu = NaturalLanguageUnderstanding() # NLU
print(naturalLanguageUnderstanding.convert_text_to_frame("Cześć, jak masz na imię?")) 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)

0
src/model/__init__.py Normal file
View File

View File

@ -1,7 +1,14 @@
from model.slot import Slot from .slot import Slot
class Frame: class Frame:
def __init__(self, source: str, act: str, slots: list[Slot]): def __init__(self, source: str, act: str, slots: list[Slot]):
self.source = source self.source = source
self.slots = slots self.slots = slots
self.act = act self.act = act
def __str__(self):
msg = f"Act: {self.act}, Slots: ["
for slot in self.slots:
msg += f"({slot}), "
msg += "]"
return msg

View File

@ -1,4 +1,7 @@
class Slot: class Slot:
def __init__(self, name, value=None): def __init__(self, name, value=None):
self.name = name self.name = name
self.value = value self.value = value
def __str__(self) -> str:
return f"Name: {self.name}, Value: {self.value}"

View File

@ -1,30 +1,30 @@
from flair.models import SequenceTagger from flair.models import SequenceTagger
from nlu_utils import predict_single, predict_multiple, predict_and_annotate from utils.nlu_utils import predict_single, predict_multiple, predict_and_annotate
# Exploratory tests # Exploratory tests
frame_model = SequenceTagger.load('frame-model/best-model.pt') frame_model = SequenceTagger.load('frame-model-prod/best-model.pt')
tests = [ tests = [
'chciałbym zamówić pizzę', 'chciałbym zamówić pizzę',
'na godzinę 12', 'na godzinę 12',
'prosiłbym o pizzę z pieczarkami', 'prosiłbym o pizzę z pieczarkami',
'to wszystko, jaka cena?', 'to wszystko, jaka cena?',
'ile kosztuje pizza', 'ile kosztuje pizza',
'do widzenia', 'do widzenia',
'tak', 'tak',
'nie dziękuję', 'nie dziękuję',
'dodatkowy ser', 'dodatkowy ser',
'pizzę barcelona bez cebuli', 'pizzę barcelona bez cebuli',
] ]
# print("=== Exploratory tests - frame model ===") print("=== Exploratory tests - frame model ===")
for test in tests: for test in tests:
print(f"Sentence: {test}") print(f"Sentence: {test}")
print(f"Single prediction: {predict_single(frame_model, test.split(), 'frame')}") print(f"Single prediction: {predict_single(frame_model, test.split(), 'frame')}")
print(f"Multiple predictions: {predict_multiple(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(f"Annotated sentence: {predict_and_annotate(frame_model, test.split(), 'frame')}")
print("=== Exploratory tests - slot model ===") print("=== Exploratory tests - slot model ===")
slot_model = SequenceTagger.load('slot-model/final-model.pt') slot_model = SequenceTagger.load('slot-model-prod/best-model.pt')
for test in tests: for test in tests:
print(f"Sentence: {test}") print(f"Sentence: {test}")
print(f"Prediction: {predict_and_annotate(slot_model, test.split(), 'slot')}") print(f"Prediction: {predict_and_annotate(slot_model, test.split(), 'slot')}")

0
src/service/__init__.py Normal file
View File

View File

@ -1,11 +1,8 @@
from model.frame import Frame from model.frame import Frame
class DialogPolicy: class DialogPolicy:
def __init__(self, frames: list[Frame]) -> None: def next_dialogue_act(self, frames: list[Frame]) -> Frame:
self.frames = frames if frames[-1].act == "welcomemsg":
def next_dialogue_act(self) -> Frame:
if self.frames[-1].act == "welcomemsg":
return Frame("system", "welcomemsg", []) return Frame("system", "welcomemsg", [])
else: else:
return Frame("system", "canthelp", []) return Frame("system", "canthelp", [])

View File

@ -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: class NaturalLanguageUnderstanding:
def __init__(self):
print("\n========================================================")
print("Models are loading, it may take a moment, please wait...")
print("========================================================\n")
dictionary = { self.frame_model = SequenceTagger.load('frame-model-prod/best-model.pt')
"Cześć," : "welcomemsg()", self.slot_model = SequenceTagger.load('slot-model-prod/best-model.pt')
"imię?" : "request(name)"
}
def convert_text_to_frame(self, text: str): print("\n========================================================")
frame = "" print("Models loaded. NLU system is ready.")
text = text.split(" ") print("========================================================\n")
for word in text:
if(word in self.dictionary): def __predict_intention(self, text: str):
frame+=self.dictionary[word]+"&" return predict_single(self.frame_model, text.split(), 'frame')
return frame[0:-1]
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

View File

@ -1,5 +1,5 @@
class NaturalLanguageGeneration: class NaturalLanguageGeneration:
def __init__(self, name): def __init__(self):
self.name = ["Michał"] self.name = ["Michał"]
def respond_to_name_query(self, question): def respond_to_name_query(self, question):

0
src/utils/__init__.py Normal file
View File

View File

@ -1,101 +1,100 @@
from flair.data import Sentence from flair.data import Sentence
from flair.datasets import FlairDatapointDataset from flair.datasets import FlairDatapointDataset
def nolabel2o(line, i): def nolabel2o(line, i):
return 'O' if line[i] == 'NoLabel' else line[i] return 'O' if line[i] == 'NoLabel' else line[i]
def conllu2flair(sentences, label=None): def conllu2flair(sentences, label=None):
if label == "frame": if label == "frame":
return conllu2flair_frame(sentences, label) return conllu2flair_frame(sentences, label)
else: else:
return conllu2flair_slot(sentences, label) return conllu2flair_slot(sentences, label)
def conllu2flair_frame(sentences, label=None): def conllu2flair_frame(sentences, label=None):
fsentences = [] fsentences = []
for sentence in sentences: for sentence in sentences:
tokens = [token["form"] for token in sentence] tokens = [token["form"] for token in sentence]
fsentence = Sentence(' '.join(tokens), use_tokenizer=False) fsentence = Sentence(' '.join(tokens), use_tokenizer=False)
for i in range(len(fsentence)): for i in range(len(fsentence)):
fsentence[i:i+1].add_label(label, sentence[i][label]) fsentence[i:i+1].add_label(label, sentence[i][label])
fsentences.append(fsentence) fsentences.append(fsentence)
return FlairDatapointDataset(fsentences) return FlairDatapointDataset(fsentences)
def conllu2flair_slot(sentences, label=None): def conllu2flair_slot(sentences, label=None):
fsentences = [] fsentences = []
for sentence in sentences: for sentence in sentences:
fsentence = Sentence(' '.join(token['form'] for token in sentence), use_tokenizer=False) fsentence = Sentence(' '.join(token['form'] for token in sentence), use_tokenizer=False)
start_idx = None start_idx = None
end_idx = None end_idx = None
tag = None tag = None
if label: if label:
for idx, (token, ftoken) in enumerate(zip(sentence, fsentence)): for idx, (token, ftoken) in enumerate(zip(sentence, fsentence)):
if token[label].startswith('B-'): if token[label].startswith('B-'):
if start_idx is not None: if start_idx is not None:
fsentence[start_idx:end_idx+1].add_label(label, tag) fsentence[start_idx:end_idx+1].add_label(label, tag)
start_idx = idx start_idx = idx
end_idx = idx end_idx = idx
tag = token[label][2:] tag = token[label][2:]
elif token[label].startswith('I-'): elif token[label].startswith('I-'):
end_idx = idx end_idx = idx
elif token[label] == 'O': elif token[label] == 'O':
if start_idx is not None: if start_idx is not None:
fsentence[start_idx:end_idx+1].add_label(label, tag) fsentence[start_idx:end_idx+1].add_label(label, tag)
start_idx = None start_idx = None
end_idx = None end_idx = None
tag = None tag = None
if start_idx is not None: if start_idx is not None:
fsentence[start_idx:end_idx+1].add_label(label, tag) fsentence[start_idx:end_idx+1].add_label(label, tag)
fsentences.append(fsentence) fsentences.append(fsentence)
return FlairDatapointDataset(fsentences) return FlairDatapointDataset(fsentences)
def __predict(model, csentence): def __predict(model, csentence):
fsentence = conllu2flair([csentence])[0] fsentence = conllu2flair([csentence])[0]
model.predict(fsentence) model.predict(fsentence)
return fsentence return fsentence
def __csentence(sentence, label_type): def __csentence(sentence, label_type):
if label_type == "frame": if label_type == "frame":
return [{'form': word } for word in sentence] return [{'form': word } for word in sentence]
else: else:
return [{'form': word, 'slot': 'O'} for word in sentence] return [{'form': word, 'slot': 'O'} for word in sentence]
def predict_single(model, sentence, label_type): def predict_single(model, sentence, label_type):
csentence = __csentence(sentence, label_type) csentence = __csentence(sentence, label_type)
fsentence = __predict(model, csentence) fsentence = __predict(model, csentence)
intent = {} intent = {}
for span in fsentence.get_spans(label_type):
for span in fsentence.get_spans(label_type): tag = span.get_label(label_type).value
tag = span.get_label(label_type).value if tag in intent:
if tag in intent: intent[tag] += 1
intent[tag] += 1 else:
else: intent[tag] = 1
intent[tag] = 1
return max(intent, key=intent.get)
return max(intent, key=intent.get)
def predict_multiple(model, sentence, label_type):
def predict_multiple(model, sentence, label_type): csentence = __csentence(sentence, label_type)
csentence = __csentence(sentence, label_type) fsentence = __predict(model, csentence)
fsentence = __predict(model, csentence)
return set(span.get_label(label_type).value for span in fsentence.get_spans(label_type))
return set(span.get_label(label_type).value for span in fsentence.get_spans(label_type))
def predict_and_annotate(model, sentence, label_type):
def predict_and_annotate(model, sentence, label_type): csentence = __csentence(sentence, label_type)
csentence = __csentence(sentence, label_type) fsentence = __predict(model, csentence)
fsentence = __predict(model, csentence)
for span in fsentence.get_spans(label_type):
for span in fsentence.get_spans(label_type): tag = span.get_label(label_type).value
tag = span.get_label(label_type).value if label_type == "frame":
if label_type == "frame": csentence[span.tokens[0].idx-1]['frame'] = tag
csentence[span.tokens[0].idx-1]['frame'] = tag else:
else: csentence[span.tokens[0].idx - 1]['slot'] = f'B-{tag}'
csentence[span.tokens[0].idx - 1]['slot'] = f'B-{tag}' for token in span.tokens[1:]:
for token in span.tokens[1:]: csentence[token.idx - 1]['slot'] = f'I-{tag}'
csentence[token.idx - 1]['slot'] = f'I-{tag}'
return csentence return csentence