Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adaptive mode patch #5

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ Usage:
Percentage of characters that will be a capital letter. Default is 0.
-r RANDOM_SEED, --random_seed RANDOM_SEED
Allows following a determinstic sequence of random values. Default is the system timer.
-A, --adaptive Adaptive mode. Store errors and adaptively offer the most common
errors first. Default is no adaptive mode.

eg:

Expand Down Expand Up @@ -126,4 +128,6 @@ Or for Dragonfly mode, to use your own Dragonfly grammar and not myne, you'll ne
...
}

Adaptive mode:

In adaptive mode it will store errors and adaptively offer failed combos randomly, most common errors first. Default is no adaptive mode.
83 changes: 83 additions & 0 deletions failed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Tue Oct 1 15:47:02 2019

This class stores failed codes and returns a random code based on the
number of times it has failed. The more times a code has failed, the
more likely it is to be returned.
"""

import random
from collections import defaultdict

DEBUG=False

def debug(*args):
""" Prints debug messages if DEBUG is True """
if DEBUG:
print(*args)

class FailedCodes:
"""
This class stores failed codes and returns a random code based on the
number of times it has failed.
"""
def __init__(self, repetitions=3):
self.repetitions = repetitions
self.fail_counter = defaultdict(int)
debug('Initialized fail_counter')

def fail(self, code):
"""
Increments the number of repetitons a code thats failed.
"""
debug('Incrementing fail_counter')
self.fail_counter[code] += self.repetitions

def unfail(self, code):
"""
Decrements the number of times a code has failed.
Parameter code speicfies probablity of unfailing a code.
"""
if code not in self.fail_counter:
debug('Code not in fail_counter')
return
if self.fail_counter[code] > 1:
debug('Decrementing fail_counter')
self.fail_counter[code] -= 1
else:
debug('Deleting code from fail_counter')
del self.fail_counter[code]

def get_num_of_repetitions(self, code):
"""
Check for fail_counter[code] existance and return the
number of repetitions.
"""
debug('Checking fail_counter')
return self.fail_counter.get(code, 0)

def random(self, pb1, pb2):
"""
Returns a random code based on the number of times it has failed.
"""
if not self.fail_counter:
debug('No codes in fail_counter')
return None
sorted_codes = sorted(self.fail_counter.items(), key=lambda x: x[1], reverse=True)
total_fails = sum(x[1] for x in sorted_codes)
prob_range = pb2 - pb1
pb_interval = prob_range / (total_fails - 1) if total_fails > 1 else 0

prob = pb1
threshold = prob
# since there is at least one code in the fail_counter
# just try forever until we get a code
while True:
debug("Trying to get a random combo from failed codes")
for code, _ in sorted_codes:
if random.random() < threshold:
return code
prob += pb_interval
threshold -= pb_interval
31 changes: 31 additions & 0 deletions failed_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""
This is a test file for the FailedCodes class.
"""

from collections import Counter
import pytest # pylint: disable=unused-import

from failed import FailedCodes

def test_failed_codes():
"""
Test the FailedCodes class.
"""
failed_codes = FailedCodes()
failed_codes.fail('a')
failed_codes.fail('a')
failed_codes.fail('b')
failed_codes.fail('c')
failed_codes.fail('d')
failed_codes.fail('d')
failed_codes.fail('d')

result_counts = Counter([failed_codes.random(0.5, 0.01) for _ in range(1000)])

# Check that the returned results are not None
assert None not in result_counts.keys()

# Check that the most frequent code is returned more often
assert result_counts['d'] > result_counts['a']
assert result_counts['d'] > result_counts['b']
assert result_counts['d'] > result_counts['c']
58 changes: 41 additions & 17 deletions practice_mappings.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env python
# A very mininal game that helps practice names of keys for Dragonfly or knausj Talon speech recognition grammars.
# By Shervin Emami 2023, "http://shervinemami.com/".
# Tested on Ubuntu 22.04 using python 3.10.
# Tested on Ubuntu 23.04 using python 3.10.

# Python 2/3 compatibility
from __future__ import print_function
Expand All @@ -13,6 +13,8 @@
import operator
import argparse

from failed import FailedCodes

# Instantiate the argument parser
parser = argparse.ArgumentParser(usage='%(prog)s [options] [combo_length]')

Expand All @@ -26,6 +28,7 @@
parser.add_argument('-c', '--no_crucial', action='store_true', help='Skip crucial symbols (commas and spaces). Default is to include crucial symbols.')
parser.add_argument('-p', '--capitals_percentage', type=int, default=0, help='Percentage of characters that will be a capital letter. Default is 0.')
parser.add_argument('-r', '--random_seed', type=int, help='Allows following a determinstic sequence of random values. Default is the system timer.')
parser.add_argument('-A', '--adaptive', action='store_true', default=False, help='Adaptive mode. Store errors and adaptively offer the most common errors first. Default is no adaptive mode.')
args = parser.parse_args()

print("Practice keyboard mappings, such as to practice voice coding. By Shervin Emami (http://shervinemami.com), 2023.")
Expand All @@ -40,6 +43,8 @@
print("Using", args.random_seed, "as the random seed instead of the current time")
random.seed(args.random_seed)

if args.adaptive:
print("Adaptive mode is enabled. Failed combos will be stored and offered later, randomly.")

if args.dragonfly:
# Import the "letterMap" dictionary from the "lettermap.py" file that's in the MacroSystem folder.
Expand Down Expand Up @@ -283,26 +288,43 @@ def load_talon_symbolmap(filename):
averagedSpeed = -1 # Initialize with the first measurement
nextAlphabet = 0

# prepare reverse letterMap
reverseLetterMap = {v: k for k, v in letterMap}
# prepare failed combos object
# TODO: probabilities and repetitions should be configurable from command line
failed_combos = FailedCodes(repetitions=3)
while (True):
truth = ""
chars = []
words = []
for i in range(combo):
if args.alphabetical:
r = nextAlphabet # Pick the next letter
nextAlphabet = nextAlphabet + 1
if nextAlphabet >= len(letterMap):
nextAlphabet = 0
else:
r = random.randint(0, len(letterMap) - 1) # Pick a random letter
(word, char) = letterMap[r]
if random.randint(0, 100) < args.capitals_percentage: # Occasionally use a capital letter
char = char.upper()
word = word.upper()
#print("%25s %25s" % (word, char))
chars.append(char)
words.append(word)
truth += char
# Get failed combo from storage first, if exists, if not, FailedCodes.random returns None
# 0.5 is the probability of getting the failed combo
# 0.1 is the probability of getting the least failed combo (but still failed)
truth_candidate = failed_combos.random(0.5, 0.1)
# in adaptive mode, devote 20% of attempts to fix failed combos
if args.adaptive and truth_candidate is not None and random.random() < 0.5:
nf = failed_combos.get_num_of_repetitions(truth_candidate)
print(f"Retrying preivously failed combo '{truth_candidate}' ({nf} repetitions to go)")
truth = truth_candidate
chars = truth
words = [reverseLetterMap[c] for c in truth]
else:
for i in range(combo):
if args.alphabetical:
r = nextAlphabet # Pick the next letter
nextAlphabet = nextAlphabet + 1
if nextAlphabet >= len(letterMap):
nextAlphabet = 0
else:
r = random.randint(0, len(letterMap) - 1) # Pick a random letter
(word, char) = letterMap[r]
if random.randint(0, 100) < args.capitals_percentage: # Occasionally use a capital letter
char = char.upper()
word = word.upper()
#print("%25s %25s" % (word, char))
chars.append(char)
words.append(word)
truth += char

# Print all the characters on a single line
for i in range(combo):
Expand Down Expand Up @@ -333,8 +355,10 @@ def load_talon_symbolmap(filename):
tallyCorrect = tallyCorrect+1
wordErrorRate = 100.0 * (tallyWrong / float(tallyCorrect + tallyWrong))
print("Correct. Tally: %d correct = %.1f%% WER. Speed: %.2f s/key" % (tallyCorrect, wordErrorRate, averagedSpeed))
failed_combos.unfail(truth)
else:
tallyWrong = tallyWrong+1
print("### WRONG! ###### ", truth, typed, "############ Tally:", tallyCorrect, "correct,", tallyWrong, "wrong. ###################################")
failed_combos.fail(truth)
print()