Natural Language Processing
Senior 6 · ICT
Natural Language Processing
Lesson 4 of 5
25 min
The keyword bot from the last lesson only knows the words you typed into its keyword lists. A retrieval-based bot works differently: you give it example conversations, it stores them, and when a new message arrives it finds the stored message that is most similar and gives the reply that followed it. You improve it by giving it more and better examples, not by editing rules. The Python library ChatterBot works this way, and in this lesson you train it on the school office's questions, measure where it goes wrong, and make it fail safely.
Definition
ChatterBot: an open-source Python library for building retrieval-based chatbots. It stores statements and the replies that followed them in a database, and answers by finding the most similar stored statement.
Training (in ChatterBot): storing example conversations, each a list of statements in which every statement is a reply to the one before it.
Confidence: a number from 0 to 1 that ChatterBot attaches to each response, measuring how similar the best stored statement was to the user's message.
Activate the virtual environment from lesson 2, then install ChatterBot and the small English language model of spaCy, the NLP library ChatterBot uses to tag words. The commands below were tested with ChatterBot 1.2.15 on Python 3.12; ChatterBot 1.2 needs Python 3.10 or newer.
(.venv) $ pip install chatterbot
(.venv) $ python -m spacy download en_core_web_smIf you skip the second command, ChatterBot stops with the message The Spacy model for "English" language is missing, which tells you exactly what to run.
Save this as train_bot.py and run it once. It creates a database file, schoolbot.sqlite3, which is the bot's memory. Notice that each answer is trained with several ways of asking the question: this is the single most effective way to improve a retrieval bot.
from chatterbot import ChatBot
from chatterbot.trainers import ListTrainer
bot = ChatBot(
"SchoolBot",
storage_adapter="chatterbot.storage.SQLStorageAdapter",
database_uri="sqlite:///schoolbot.sqlite3", # the bot's memory, a file
logic_adapters=[{
"import_path": "chatterbot.logic.BestMatch",
"default_response": "Sorry, I do not know that yet. Please ask the school office.",
"maximum_similarity_threshold": 0.90,
}],
)
# Each answer is trained with several ways of asking the question
FAQ = {
"Term 1 begins on Monday 7 September.": [
"When does term start?", "When do classes begin?",
"When does school open?", "What is the first day of term?"],
"The fee for one term is 95,000 Frw.": [
"How much are the school fees?", "How much are the fees?",
"What are the fees?", "How much does one term cost?"],
"Bring your last report, a copy of your birth certificate and two photos.": [
"What documents should I bring?", "Which documents do I need?",
"What papers are needed to join?"],
}
trainer = ListTrainer(bot, show_training_progress=False)
for answer, questions in FAQ.items():
for question in questions:
trainer.train([question, answer])
print("Trained on", sum(len(q) for q in FAQ.values()), "question-answer pairs")
Output:
Trained on 11 question-answer pairsSave this as ask_bot.py. It opens the same database in read-only mode and wraps every request in checks: empty or very long input, unexpected errors, and low confidence. Questions it cannot answer confidently are logged to unanswered.txt so that the office can add them to the training data.
from chatterbot import ChatBot
FALLBACK = "Sorry, I do not know that yet. Please ask the school office."
bot = ChatBot("SchoolBot",
storage_adapter="chatterbot.storage.SQLStorageAdapter",
database_uri="sqlite:///schoolbot.sqlite3",
logic_adapters=[{
"import_path": "chatterbot.logic.BestMatch",
"default_response": FALLBACK,
"maximum_similarity_threshold": 0.90,
}],
read_only=True) # users' messages never change what it knows
def answer(question, min_confidence=0.5):
question = question.strip()
if not question:
return "Please type a question."
if len(question) > 300:
return "That message is too long. Please ask one short question."
try:
response = bot.get_response(question)
except Exception: # never let the user see a crash
return "Something went wrong. Please try again later."
if response.confidence < min_confidence:
with open("unanswered.txt", "a", encoding="utf-8") as log:
log.write(question + "\n") # for the office to review
return FALLBACK
return str(response)
if __name__ == "__main__":
for q in ["When do classes start?", "What are the fees?", "What documents do I need?",
"How much is the uniform?", "", "When does the school open?"]:
print(f"{q!r:30} -> {answer(q)}")
Output, as run:
'When do classes start?' -> Term 1 begins on Monday 7 September.
'What are the fees?' -> The fee for one term is 95,000 Frw.
'What documents do I need?' -> Bring your last report, a copy of your birth certificate and two photos.
'How much is the uniform?' -> The fee for one term is 95,000 Frw.
'' -> Please type a question.
'When does the school open?' -> Term 1 begins on Monday 7 September.Five answers are right. One is badly wrong: "How much is the uniform?" was matched to the fees question, because "How much is…" looks like "How much are…", and the bot was never taught anything about uniforms. It answered with confidence 0.70, above the threshold of 0.5, so the safety check did not stop it.
Worked example 1: choosing the confidence threshold
The confidences ChatterBot reported for four test questions were:
| Question | Matched answer | Correct? | Confidence |
|---|---|---|---|
| When does the school open? | Term dates | Yes | 0.92 |
| What documents do I need? | Documents | Yes | 0.90 |
| When do classes start? | Term dates | Yes | 0.74 |
| How much is the uniform? | Fees | No | 0.70 |
min_confidence=0.5, all four are answered: one wrong answer is given.min_confidence=0.75 (tested), the uniform question is refused, but so is "When do classes start?", a question the bot had answered correctly. Both are logged.| Problem | Improvement |
|---|---|
| Paraphrases are not recognised | Train each answer with several phrasings, including the ones in unanswered.txt |
| Unknown topics get a similar-looking wrong answer | Add answers for common topics; set a confidence threshold; give a clear fallback that hands over to a person |
| Users teach it rubbish | Use read_only=True in the live bot; train only from reviewed data |
| Spelling mistakes | Correct words against the bot's vocabulary before matching (below) |
| The program crashes on odd input | Check empty and over-long input; catch exceptions and show a polite message |
| You cannot tell if a change helped | Keep a fixed list of test questions and expected answers; re-run it after every change |
A warning from history: in 2016 Microsoft released a chatbot called Tay that learned from conversations with the public on Twitter. Within a day users had taught it to post offensive messages, and Microsoft took it offline. A bot that learns from unreviewed input learns whatever it is fed.
Python's built-in difflib module can find the closest known word to a misspelt one. Added to the keyword bot from lesson 3, it lets "feez" match "fee":
from difflib import get_close_matches
from schoolbot import INTENTS, normalise
VOCABULARY = set().union(*(i["keywords"] for i in INTENTS.values()))
def correct(words):
"""Replace each unknown word by the closest keyword, if one is close enough."""
fixed = set()
for w in words:
match = get_close_matches(w, VOCABULARY, n=1, cutoff=0.8)
fixed.add(match[0] if match else w)
return fixed
for msg in ["feez?", "documants to bring", "wen does term begn"]:
print(f"{msg!r:22} -> {sorted(correct(normalise(msg)))}")
'feez?' -> ['fee']
'documants to bring' -> ['bring', 'documents', 'to']
'wen does term begn' -> ['begin', 'does', 'term', 'wen']"wen" was not corrected, because no keyword is close to it: "when" is not a keyword. The cutoff=0.8 sets how similar a word must be; lower values correct more words but also make more wrong corrections.
Worked example 2: why read_only=True?
Without read_only=True, ChatterBot stores every user message as a reply to the bot's previous statement. A student types "The fees are free this term!" after the bot asks a question.
Training only from reviewed data, and running the live bot read-only, keeps every possible answer under the school's control.
Warning
Never put secrets or personal data in the training data. Anything in the database can come back as an answer to someone. Fees, dates and procedures are fine; students' marks, phone numbers or health information are not.
Tip
The log is your teacher. Read unanswered.txt every week. Each line is a real question your bot failed on: write the approved answer, add the question and two rephrasings to the training data, retrain, and re-run your tests.
train_bot.py, then ask_bot.py.response.confidence).schoolbot.sqlite3, retrain, and re-run the tests.Check: after improvement, the bot gives no wrong answer on your test list, and every refused question appears in unanswered.txt.
Practice
trainer.train(["When can parents visit?", "On the first Sunday of each month."]) store?if response.confidence < min_confidence: achieve? What is the cost of setting min_confidence too high?answer() handles without crashing.get_response (and low-confidence matches).Sources and credits
This lesson adapts CC BY-SA material and is shared under CC BY-SA 4.0.