Finishing up NLU module
This commit is contained in:
parent
f0c7b481d1
commit
6ca7b66fb4
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
__pycache__
|
||||||
|
slot-model*
|
||||||
|
frame-model*
|
||||||
|
nlu_cache
|
11
README.md
11
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 |
|
| 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`
|
||||||
|
@ -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')
|
||||||
|
@ -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
5
requirements.txt
Normal 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
0
src/__init__.py
Normal file
48
src/main.py
48
src/main.py
@ -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
0
src/model/__init__.py
Normal 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
|
||||||
|
@ -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}"
|
@ -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
0
src/service/__init__.py
Normal 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", [])
|
||||||
|
@ -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
|
@ -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
0
src/utils/__init__.py
Normal 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
|
Loading…
Reference in New Issue
Block a user