Introduction

The overall goal of this project is to build a word recognizer for American Sign Language video sequences, demonstrating the power of probabalistic models. In particular, this project employs hidden Markov models (HMM's) to analyze a series of measurements taken from videos of American Sign Language (ASL) collected for research (see the RWTH-BOSTON-104 Database). In this video, the right-hand x and y locations are plotted as the speaker signs the sentence. ASLR demo

The raw data, train, and test sets are pre-defined. You will derive a variety of feature sets (explored in Part 1), as well as implement three different model selection criterion to determine the optimal number of hidden states for each word model (explored in Part 2). Finally, in Part 3 you will implement the recognizer and compare the effects the different combinations of feature sets and model selection criteria.

At the end of each Part, complete the submission cells with implementations, answer all questions, and pass the unit tests. Then submit the completed notebook for review!

PART 1: Data

Features Tutorial

Load the initial database

A data handler designed for this database is provided in the student codebase as the AslDb class in the asl_data module. This handler creates the initial pandas dataframe from the corpus of data included in the data directory as well as dictionaries suitable for extracting data in a format friendly to the hmmlearn library. We'll use those to create models in Part 2.

To start, let's set up the initial database and select an example set of features for the training set. At the end of Part 1, you will create additional feature sets for experimentation.

In [1]:
import numpy as np
import pandas as pd
from asl_data import AslDb


asl = AslDb() # initializes the database
asl.df.head() # displays the first five rows of the asl database, indexed by video and frame
Out[1]:
left-x left-y right-x right-y nose-x nose-y speaker
video frame
98 0 149 181 170 175 161 62 woman-1
1 149 181 170 175 161 62 woman-1
2 149 181 170 175 161 62 woman-1
3 149 181 170 175 161 62 woman-1
4 149 181 170 175 161 62 woman-1
In [2]:
asl.df.ix[98,1]  # look at the data available for an individual frame
Out[2]:
left-x         149
left-y         181
right-x        170
right-y        175
nose-x         161
nose-y          62
speaker    woman-1
Name: (98, 1), dtype: object

The frame represented by video 98, frame 1 is shown here: Video 98

Feature selection for training the model

The objective of feature selection when training a model is to choose the most relevant variables while keeping the model as simple as possible, thus reducing training time. We can use the raw features already provided or derive our own and add columns to the pandas dataframe asl.df for selection. As an example, in the next cell a feature named 'grnd-ry' is added. This feature is the difference between the right-hand y value and the nose y value, which serves as the "ground" right y value.

In [3]:
from asl_utils import test_features_tryit

asl.df['grnd-ry'] = asl.df['right-y'] - asl.df['nose-y']
asl.df['grnd-rx'] = asl.df['right-x'] - asl.df['nose-x']
asl.df['grnd-ly'] = asl.df['left-y'] - asl.df['nose-y']
asl.df['grnd-lx'] = asl.df['left-x'] - asl.df['nose-x']

# test the code
test_features_tryit(asl)
asl.df sample
left-x left-y right-x right-y nose-x nose-y speaker grnd-ry grnd-rx grnd-ly grnd-lx
video frame
98 0 149 181 170 175 161 62 woman-1 113 9 119 -12
1 149 181 170 175 161 62 woman-1 113 9 119 -12
2 149 181 170 175 161 62 woman-1 113 9 119 -12
3 149 181 170 175 161 62 woman-1 113 9 119 -12
4 149 181 170 175 161 62 woman-1 113 9 119 -12
Out[3]:
Correct!
In [4]:
# collect the features into a list
features_ground = ['grnd-rx','grnd-ry','grnd-lx','grnd-ly']
 #show a single set of features for a given (video, frame) tuple
[asl.df.ix[98,1][v] for v in features_ground]
Out[4]:
[9, 113, -12, 119]
Build the training set

Now that we have a feature list defined, we can pass that list to the build_training method to collect the features for all the words in the training set. Each word in the training set has multiple examples from various videos. Below we can see the unique words that have been loaded into the training set:

In [5]:
training = asl.build_training(features_ground)
print("Training words: {}".format(training.words))
Training words: ['JOHN', 'WRITE', 'HOMEWORK', 'IX-1P', 'SEE', 'YESTERDAY', 'IX', 'LOVE', 'MARY', 'CAN', 'GO', 'GO1', 'FUTURE', 'GO2', 'PARTY', 'FUTURE1', 'HIT', 'BLAME', 'FRED', 'FISH', 'WONT', 'EAT', 'BUT', 'CHICKEN', 'VEGETABLE', 'CHINA', 'PEOPLE', 'PREFER', 'BROCCOLI', 'LIKE', 'LEAVE', 'SAY', 'BUY', 'HOUSE', 'KNOW', 'CORN', 'CORN1', 'THINK', 'NOT', 'PAST', 'LIVE', 'CHICAGO', 'CAR', 'SHOULD', 'DECIDE', 'VISIT', 'MOVIE', 'WANT', 'SELL', 'TOMORROW', 'NEXT-WEEK', 'NEW-YORK', 'LAST-WEEK', 'WILL', 'FINISH', 'ANN', 'READ', 'BOOK', 'CHOCOLATE', 'FIND', 'SOMETHING-ONE', 'POSS', 'BROTHER', 'ARRIVE', 'HERE', 'GIVE', 'MAN', 'NEW', 'COAT', 'WOMAN', 'GIVE1', 'HAVE', 'FRANK', 'BREAK-DOWN', 'SEARCH-FOR', 'WHO', 'WHAT', 'LEG', 'FRIEND', 'CANDY', 'BLUE', 'SUE', 'BUY1', 'STOLEN', 'OLD', 'STUDENT', 'VIDEOTAPE', 'BORROW', 'MOTHER', 'POTATO', 'TELL', 'BILL', 'THROW', 'APPLE', 'NAME', 'SHOOT', 'SAY-1P', 'SELF', 'GROUP', 'JANA', 'TOY1', 'MANY', 'TOY', 'ALL', 'BOY', 'TEACHER', 'GIRL', 'BOX', 'GIVE2', 'GIVE3', 'GET', 'PUTASIDE']

The training data in training is an object of class WordsData defined in the asl_data module. in addition to the words list, data can be accessed with the get_all_sequences, get_all_Xlengths, get_word_sequences, and get_word_Xlengths methods. We need the get_word_Xlengths method to train multiple sequences with the hmmlearn library. In the following example, notice that there are two lists; the first is a concatenation of all the sequences(the X portion) and the second is a list of the sequence lengths(the Lengths portion).

In [6]:
#training.get_word_Xlengths('CHOCOLATE')
More feature sets

So far we have a simple feature set that is enough to get started modeling. However, we might get better results if we manipulate the raw values a bit more, so we will go ahead and set up some other options now for experimentation later. For example, we could normalize each speaker's range of motion with grouped statistics using Pandas stats functions and pandas groupby. Below is an example for finding the means of all speaker subgroups.

Try it!
In [7]:
from asl_utils import test_std_tryit
# TODO Create a dataframe named `df_std` with standard deviations grouped by speaker
df_std = asl.df.groupby('speaker').std()
df_std
asl.df['left-x-std']= asl.df['speaker'].map(df_std['left-x'])
asl.df['grnd-lx-std']= asl.df['speaker'].map(df_std['grnd-lx'])
asl.df['left-y-std']= asl.df['speaker'].map(df_std['left-y'])
asl.df['grnd-ly-std']= asl.df['speaker'].map(df_std['grnd-ly'])
asl.df['right-x-std']= asl.df['speaker'].map(df_std['right-x'])
asl.df['grnd-rx-std']= asl.df['speaker'].map(df_std['grnd-rx'])
asl.df['right-y-std']= asl.df['speaker'].map(df_std['right-y'])
asl.df['grnd-ry-std']= asl.df['speaker'].map(df_std['grnd-ry'])



df_means = asl.df.groupby('speaker').mean()
df_means
asl.df['left-x-mean']= asl.df['speaker'].map(df_means['left-x'])
asl.df['grnd-lx-mean']= asl.df['speaker'].map(df_means['grnd-lx'])
asl.df['left-y-mean']= asl.df['speaker'].map(df_means['left-y'])
asl.df['grnd-ly-mean']= asl.df['speaker'].map(df_means['grnd-ly'])
asl.df['right-x-mean']= asl.df['speaker'].map(df_means['right-x'])
asl.df['grnd-rx-mean']= asl.df['speaker'].map(df_means['grnd-rx'])
asl.df['right-y-mean']= asl.df['speaker'].map(df_means['right-y'])
asl.df['grnd-ry-mean']= asl.df['speaker'].map(df_means['grnd-ry'])
asl.df.head()


# test the code
#test_std_tryit(df_std)
Out[7]:
left-x left-y right-x right-y nose-x nose-y speaker grnd-ry grnd-rx grnd-ly ... right-y-std grnd-ry-std left-x-mean grnd-lx-mean left-y-mean grnd-ly-mean right-x-mean grnd-rx-mean right-y-mean grnd-ry-mean
video frame
98 0 149 181 170 175 161 62 woman-1 113 9 119 ... 34.667787 33.97266 164.661438 2.006318 161.271242 104.026144 151.017865 -11.637255 117.332462 60.087364
1 149 181 170 175 161 62 woman-1 113 9 119 ... 34.667787 33.97266 164.661438 2.006318 161.271242 104.026144 151.017865 -11.637255 117.332462 60.087364
2 149 181 170 175 161 62 woman-1 113 9 119 ... 34.667787 33.97266 164.661438 2.006318 161.271242 104.026144 151.017865 -11.637255 117.332462 60.087364
3 149 181 170 175 161 62 woman-1 113 9 119 ... 34.667787 33.97266 164.661438 2.006318 161.271242 104.026144 151.017865 -11.637255 117.332462 60.087364
4 149 181 170 175 161 62 woman-1 113 9 119 ... 34.667787 33.97266 164.661438 2.006318 161.271242 104.026144 151.017865 -11.637255 117.332462 60.087364

5 rows × 27 columns

Features Implementation Submission

Implement four feature sets and answer the question that follows.

  • normalized Cartesian coordinates

    • use mean and standard deviation statistics and the standard score equation to account for speakers with different heights and arm length
  • polar coordinates

    • calculate polar coordinates with Cartesian to polar equations
    • use the np.arctan2 function and swap the x and y axes to move the $0$ to $2\pi$ discontinuity to 12 o'clock instead of 3 o'clock; in other words, the normal break in radians value from $0$ to $2\pi$ occurs directly to the left of the speaker's nose, which may be in the signing area and interfere with results. By swapping the x and y axes, that discontinuity move to directly above the speaker's head, an area not generally used in signing.
  • delta difference

    • as described in Thad's lecture, use the difference in values between one frame and the next frames as features
    • pandas diff method and fillna method will be helpful for this one
  • custom features

    • These are your own design; combine techniques used above or come up with something else entirely. We look forward to seeing what you come up with! Some ideas to get you started:
In [8]:
# TODO add features for normalized by speaker values of left, right, x, y
# Name these 'norm-rx', 'norm-ry', 'norm-lx', and 'norm-ly'
# using Z-score scaling (X-Xmean)/Xstd
import numpy as np 
features_norm = ['norm-rx', 'norm-ry', 'norm-lx','norm-ly']
asl.df['norm-lx'] = ((asl.df['left-x'] - asl.df['left-x-mean'] ) / asl.df['left-x-std'])
asl.df['norm-ly'] = ((asl.df['left-y'] - asl.df['left-y-mean'] ) / asl.df['left-y-std'])
asl.df['norm-rx'] = ((asl.df['right-x'] - asl.df['right-x-mean'] ) / asl.df['right-x-std'])
asl.df['norm-ry'] = ((asl.df['right-y'] - asl.df['right-y-mean'] ) / asl.df['right-y-std'])
asl.df.columns
Out[8]:
Index(['left-x', 'left-y', 'right-x', 'right-y', 'nose-x', 'nose-y', 'speaker',
       'grnd-ry', 'grnd-rx', 'grnd-ly', 'grnd-lx', 'left-x-std', 'grnd-lx-std',
       'left-y-std', 'grnd-ly-std', 'right-x-std', 'grnd-rx-std',
       'right-y-std', 'grnd-ry-std', 'left-x-mean', 'grnd-lx-mean',
       'left-y-mean', 'grnd-ly-mean', 'right-x-mean', 'grnd-rx-mean',
       'right-y-mean', 'grnd-ry-mean', 'norm-lx', 'norm-ly', 'norm-rx',
       'norm-ry'],
      dtype='object')
In [9]:
# TODO add features for polar coordinate values where the nose is the origin
# Name these 'polar-rr', 'polar-rtheta', 'polar-lr', and 'polar-ltheta'
# Note that 'polar-rr' and 'polar-rtheta' refer to the radius and angle

features_polar = ['polar-rr', 'polar-rtheta', 'polar-lr', 'polar-ltheta']

def cart2pol(x, y):
    rho = np.sqrt(x**2 + y**2)
    phi = np.arctan2(x,y)
    return(rho, phi)

def pol2cart(rho, phi):
    x = rho * np.cos(phi)
    y = rho * np.sin(phi)
    return(x, y)

features_polar = ['polar-rr', 'polar-rtheta', 'polar-lr', 'polar-ltheta']


(r,p) = cart2pol(asl.df['grnd-rx'],asl.df['grnd-ry'])
asl.df['polar-rr'] = r
asl.df['polar-rtheta'] = p

(r,p) = cart2pol(asl.df['grnd-lx'],asl.df['grnd-ly'])
asl.df['polar-lr'] = r
asl.df['polar-ltheta'] = p


df_std = asl.df.groupby('speaker').std()
df_mean = asl.df.groupby('speaker').mean()

asl.df['polar-rr-std']= asl.df['speaker'].map(df_std['polar-rr'])
asl.df['polar-lr-std']= asl.df['speaker'].map(df_std['polar-lr'])
asl.df['polar-ltheta-std']= asl.df['speaker'].map(df_std['polar-ltheta'])
asl.df['polar-rtheta-std']= asl.df['speaker'].map(df_std['polar-rtheta'])


asl.df['polar-rr-mean']= asl.df['speaker'].map(df_mean['polar-rr'])
asl.df['polar-lr-mean']= asl.df['speaker'].map(df_mean['polar-lr'])
asl.df['polar-ltheta-mean']= asl.df['speaker'].map(df_mean['polar-ltheta'])
asl.df['polar-rtheta-mean']= asl.df['speaker'].map(df_mean['polar-rtheta'])

asl.df['norm-polar-rr'] = ((asl.df['polar-rr'] - asl.df['polar-rr-mean'] ) / asl.df['polar-rr-std'])
asl.df['norm-polar-lr'] = ((asl.df['polar-lr'] - asl.df['polar-lr-mean'] ) / asl.df['polar-lr-std'])
asl.df['norm-polar-ltheta'] = ((asl.df['polar-ltheta'] - asl.df['polar-ltheta-mean'] ) / asl.df['polar-ltheta-std'])
asl.df['norm-polar-rtheta'] = ((asl.df['polar-rtheta'] - asl.df['polar-rtheta-mean'] ) / asl.df['polar-rtheta-std'])

features_custom = ['norm-polar-lr','norm-polar-rr','norm-polar-ltheta','norm-polar-rtheta']
In [10]:
# TODO add features for left, right, x, y differences by one time step, i.e. the "delta" values discussed in the lecture
# Name these 'delta-rx', 'delta-ry', 'delta-lx', and 'delta-ly'
asl.df['delta-rx'] = asl.df['right-x'].diff(periods=1)
asl.df['delta-ry'] = asl.df['right-y'].diff(periods=1)
asl.df['delta-lx'] = asl.df['left-x'].diff(periods=1)
asl.df['delta-ly'] = asl.df['left-y'].diff(periods=1)
features_delta = ['delta-rx', 'delta-ry', 'delta-lx', 'delta-ly']
sample = (asl.df.ix[98, 18][features_delta]).tolist()
#sample

asl.df.fillna(0, inplace=True)
#asl.df
In [40]:
# TODO add features of your own design, which may be a combination of the above or something else
# Name these whatever you would like

# TODO define a list named 'features_custom' for building the training set
import sklearn

features_ground_normal = ['grnd-rx-norm','grnd-ry-norm', 'grnd-lx-norm', 'grnd-ly-norm']

asl.df['grnd-rx-norm'] = ((asl.df['grnd-rx'] - asl.df['grnd-rx-mean'] ) / asl.df['grnd-rx-std'])
asl.df['grnd-ry-norm'] = ((asl.df['grnd-ry'] - asl.df['grnd-ry-mean'] ) / asl.df['grnd-ry-std'])
asl.df['grnd-lx-norm'] = ((asl.df['grnd-lx'] - asl.df['grnd-lx-mean'] ) / asl.df['grnd-lx-std'])
asl.df['grnd-ly-norm'] = ((asl.df['grnd-ly'] - asl.df['grnd-ly-mean'] ) / asl.df['grnd-ly-std'])

asl.df.head()
#features_ground_normal = sklearn.preprocessing.normalize(features_ground)
Out[40]:
left-x left-y right-x right-y nose-x nose-y speaker grnd-ry grnd-rx grnd-ly ... polar-lr polar-ltheta delta-rx delta-ry delta-lx delta-ly grnd-rx-norm grnd-ry-norm grnd-lx-norm grnd-ly-norm
video frame
98 0 149 181 170 175 161 62 woman-1 113 9 119 ... 119.604 -0.101 0.000 0.000 0.000 0.000 1.231 1.558 -0.808 0.552
1 149 181 170 175 161 62 woman-1 113 9 119 ... 119.604 -0.101 0.000 0.000 0.000 0.000 1.231 1.558 -0.808 0.552
2 149 181 170 175 161 62 woman-1 113 9 119 ... 119.604 -0.101 0.000 0.000 0.000 0.000 1.231 1.558 -0.808 0.552
3 149 181 170 175 161 62 woman-1 113 9 119 ... 119.604 -0.101 0.000 0.000 0.000 0.000 1.231 1.558 -0.808 0.552
4 149 181 170 175 161 62 woman-1 113 9 119 ... 119.604 -0.101 0.000 0.000 0.000 0.000 1.231 1.558 -0.808 0.552

5 rows × 43 columns

Question 1: What custom features did you choose for the features_custom set and why?

Answer 1: For the custom features I chose to use ground variables, which is the nose minus the left or right variable. This effectively makes the 'nose' the zero position, and changes are detected as difference from the ground, creating an anchor point for calculations. For the custom features I chose to normalized the ground variables.

Features Unit Testing

Run the following unit tests as a sanity check on the defined "ground", "norm", "polar", and 'delta" feature sets. The test simply looks for some valid values but is not exhaustive. However, the project should not be submitted if these tests don't pass.

In [12]:
import unittest
pd.set_option('display.float_format', lambda x: '%.3f' % x)
# import numpy as np

class TestFeatures(unittest.TestCase):

    def test_features_ground(self):
        sample = (asl.df.ix[98, 1][features_ground]).tolist()
        self.assertEqual(sample, [9, 113, -12, 119])

    def test_features_norm(self):
        sample = (asl.df.ix[98, 1][features_norm]).tolist()
        np.testing.assert_almost_equal(sample, [ 1.153,  1.663, -0.891,  0.742], 3)

    def test_features_polar(self):
        sample = (asl.df.ix[98,1][features_polar]).tolist()
        np.testing.assert_almost_equal(sample, [113.3578, 0.0794, 119.603, -0.1005], 3)

    def test_features_delta(self):
        sample = (asl.df.ix[98, 0][features_delta]).tolist()
        self.assertEqual(sample, [0, 0, 0, 0])
        sample = (asl.df.ix[98, 18][features_delta]).tolist()
        self.assertTrue(sample in [[-16, -5, -2, 4], [-14, -9, 0, 0]], "Sample value found was {}".format(sample))
                         
suite = unittest.TestLoader().loadTestsFromModule(TestFeatures())
unittest.TextTestRunner().run(suite)
....
----------------------------------------------------------------------
Ran 4 tests in 0.036s

OK
Out[12]:
<unittest.runner.TextTestResult run=4 errors=0 failures=0>

PART 2: Model Selection

Model Selection Tutorial

The objective of Model Selection is to tune the number of states for each word HMM prior to testing on unseen data. In this section you will explore three methods:

  • Log likelihood using cross-validation folds (CV)
  • Bayesian Information Criterion (BIC)
  • Discriminative Information Criterion (DIC)
Train a single word

Now that we have built a training set with sequence data, we can "train" models for each word. As a simple starting example, we train a single word using Gaussian hidden Markov models (HMM). By using the fit method during training, the Baum-Welch Expectation-Maximization (EM) algorithm is invoked iteratively to find the best estimate for the model for the number of hidden states specified from a group of sample seequences. For this example, we assume the correct number of hidden states is 3, but that is just a guess. How do we know what the "best" number of states for training is? We will need to find some model selection technique to choose the best parameter.

In [13]:
import warnings
from hmmlearn.hmm import GaussianHMM

def train_a_word(word, num_hidden_states, features):
    
    warnings.filterwarnings("ignore", category=DeprecationWarning)
    training = asl.build_training(features)  
    X, lengths = training.get_word_Xlengths(word)
    model = GaussianHMM(n_components=num_hidden_states, n_iter=1000).fit(X, lengths)
    logL = model.score(X, lengths)
    return model, logL

demoword = 'BOOK'
model, logL = train_a_word(demoword, 4, features_ground)
print("Number of states trained in model for {} is {}".format(demoword, model.n_components))
print("logL = {}".format(logL))
Number of states trained in model for BOOK is 4
logL = -2282.0082103129193

The HMM model has been trained and information can be pulled from the model, including means and variances for each feature and hidden state. The log likelihood for any individual sample or group of samples can also be calculated with the score method.

In [14]:
def show_model_stats(word, model):
    print("Number of states trained in model for {} is {}".format(word, model.n_components))    
    variance=np.array([np.diag(model.covars_[i]) for i in range(model.n_components)])    
    for i in range(model.n_components):  # for each hidden state
        print("hidden state #{}".format(i))
        print("mean = ", model.means_[i])
        print("variance = ", variance[i])
        print()
    
show_model_stats(demoword, model)
Number of states trained in model for BOOK is 4
hidden state #0
mean =  [-10.55841733  86.78828738  18.59868813  96.40777057]
variance =  [ 73.80799477  36.26780522  31.01568112  52.7305317 ]

hidden state #1
mean =  [ -3.46504228  50.66707078  14.02390954  52.04753805]
variance =  [ 49.1235066   43.05033359  39.35088732  47.24516329]

hidden state #2
mean =  [ -13.93672327  114.42607932   20.24703657  118.28522234]
variance =  [  78.98272144  105.52761516   12.67208262   91.21104392]

hidden state #3
mean =  [ -1.12290864  69.44077025  17.02839996  77.72276398]
variance =  [ 19.68754051  16.81757949  30.51980065  11.03005485]

Try it!

Experiment by changing the feature set, word, and/or num_hidden_states values in the next cell to see changes in values.

In [15]:
my_testword = 'CHOCOLATE'
model, logL = train_a_word(my_testword, 4, features_delta) # Experiment here with different parameters
show_model_stats(my_testword, model)
print("logL = {}".format(logL))
Number of states trained in model for CHOCOLATE is 4
hidden state #0
mean =  [ 0.08311838  4.72533222  0.66510127  3.48705962]
variance =  [ 15.42203723  14.95478384  11.41019265  13.59230159]

hidden state #1
mean =  [-0.54251183 -0.1357141   0.          0.        ]
variance =  [  7.40788567e+00   2.07995784e-01   4.54704133e-04   4.54704133e-04]

hidden state #2
mean =  [ 4.99999999 -3.39999998 -1.99999999 -8.8       ]
variance =  [ 11.60200002   5.04200006   5.20200002  13.36199999]

hidden state #3
mean =  [ 0.81860546  1.18409834  0.51646389 -8.83562152]
variance =  [  2.32970546   1.61435802   9.60434654  17.06517278]

logL = -242.4397644611478
Visualize the hidden states

We can plot the means and variances for each state and feature. Try varying the number of states trained for the HMM model and examine the variances. Are there some models that are "better" than others? How can you tell? We would like to hear what you think in the classroom online.

In [16]:
%matplotlib inline
ModelSelector class

Review the SelectorModel class from the codebase found in the my_model_selectors.py module. It is designed to be a strategy pattern for choosing different model selectors. For the project submission in this section, subclass SelectorModel to implement the following model selectors. In other words, you will write your own classes/functions in the my_model_selectors.py module and run them from this notebook:

  • SelectorCV: Log likelihood with CV
  • SelectorBIC: BIC
  • SelectorDIC: DIC

You will train each word in the training set with a range of values for the number of hidden states, and then score these alternatives with the model selector, choosing the "best" according to each strategy. The simple case of training with a constant value for n_components can be called using the provided SelectorConstant subclass as follow:

In [17]:
from my_model_selectors import SelectorConstant

training = asl.build_training(features_ground)  # Experiment here with different feature sets defined in part 1
word = 'VEGETABLE' # Experiment here with different words
model = SelectorConstant(training.get_all_sequences(), training.get_all_Xlengths(), word, n_constant=3).select()
print("Number of states trained in model for {} is {}".format(word, model.n_components))
Number of states trained in model for VEGETABLE is 3
Cross-validation folds

If we simply score the model with the Log Likelihood calculated from the feature sequences it has been trained on, we should expect that more complex models will have higher likelihoods. However, that doesn't tell us which would have a better likelihood score on unseen data. The model will likely be overfit as complexity is added. To estimate which topology model is better using only the training data, we can compare scores using cross-validation. One technique for cross-validation is to break the training set into "folds" and rotate which fold is left out of training. The "left out" fold scored. This gives us a proxy method of finding the best model to use on "unseen data". In the following example, a set of word sequences is broken into three folds using the scikit-learn Kfold class object. When you implement SelectorCV, you will use this technique.

In [18]:
from sklearn.model_selection import KFold

training = asl.build_training(features_ground) # Experiment here with different feature sets
word = 'VEGETABLE' # Experiment here with different words
word_sequences = training.get_word_sequences(word)
split_method = KFold()
for cv_train_idx, cv_test_idx in split_method.split(word_sequences):
    print("Train fold indices:{} Test fold indices:{}".format(cv_train_idx, cv_test_idx))  # view indices of the folds
Train fold indices:[2 3 4 5] Test fold indices:[0 1]
Train fold indices:[0 1 4 5] Test fold indices:[2 3]
Train fold indices:[0 1 2 3] Test fold indices:[4 5]

Tip: In order to run hmmlearn training using the X,lengths tuples on the new folds, subsets must be combined based on the indices given for the folds. A helper utility has been provided in the asl_utils module named combine_sequences for this purpose.

Scoring models with other criterion

Scoring model topologies with BIC balances fit and complexity within the training set for each word. In the BIC equation, a penalty term penalizes complexity to avoid overfitting, so that it is not necessary to also use cross-validation in the selection process. There are a number of references on the internet for this criterion. These slides include a formula you may find helpful for your implementation.

The advantages of scoring model topologies with DIC over BIC are presented by Alain Biem in this reference (also found here). DIC scores the discriminant ability of a training set for one word against competing words. Instead of a penalty term for complexity, it provides a penalty if model liklihoods for non-matching words are too similar to model likelihoods for the correct word in the word set.

Model Selection Implementation Submission

Implement SelectorCV, SelectorBIC, and SelectorDIC classes in the my_model_selectors.py module. Run the selectors on the following five words. Then answer the questions about your results.

Tip: The hmmlearn library may not be able to train or score all models. Implement try/except contructs as necessary to eliminate non-viable models from consideration.

In [19]:
from my_model_selectors import SelectorCV
import asl_utils
 
words_to_train = ['FISH', 'BOOK', 'VEGETABLE', 'FUTURE', 'JOHN']
import timeit
training = asl.build_training(features_ground)  # Experiment here with different feature sets defined in part 1
sequences = training.get_all_sequences()
Xlengths = training.get_all_Xlengths()
for word in words_to_train:
    start = timeit.default_timer()
    model = SelectorCV(sequences, Xlengths, word, 
                    min_n_components=2, max_n_components=15, random_state = 14).select()
    end = timeit.default_timer()-start
    if model is not None:
        #print(model)
        print("Training complete for {} with {} states with time {} seconds".format(word, model.n_components, end))
    else:
        print("Training failed for {}".format(word))
Training complete for FISH with 13 states with time 2.8730003079999733 seconds
Training complete for BOOK with 5 states with time 20.479355002000375 seconds
Training complete for VEGETABLE with 2 states with time 8.036964130000342 seconds
Training complete for FUTURE with 2 states with time 20.955314179998823 seconds
Training complete for JOHN with 12 states with time 195.11318996099908 seconds
In [21]:
#%load_ext autoreload
#%autoreload 2
# TODO: Implement SelectorBIC in module my_model_selectors.py
from my_model_selectors import SelectorBIC

training = asl.build_training(features_ground)  # Experiment here with different feature sets defined in part 1
sequences = training.get_all_sequences()
Xlengths = training.get_all_Xlengths()
for word in words_to_train:
    start = timeit.default_timer()
    model = SelectorBIC(sequences, Xlengths, word, 
                    min_n_components=2, max_n_components=14, random_state = 14).select()
    end = timeit.default_timer()-start
    if model is not None:
        print("Training complete for {} with {} states with time {} seconds".format(word, model.n_components, end))
    else:
        print("Training failed for {}".format(word))
Training complete for FISH with 5 states with time 2.202377591000186 seconds
Training complete for BOOK with 8 states with time 11.34471683800075 seconds
Training complete for VEGETABLE with 9 states with time 4.230498508999517 seconds
Training complete for FUTURE with 9 states with time 12.409654928000236 seconds
Training complete for JOHN with 13 states with time 120.1105366060001 seconds
In [22]:
#%load_ext autoreload
#%autoreload 2
# TODO: Implement SelectorDIC in module my_model_selectors.py
from my_model_selectors import SelectorDIC

training = asl.build_training(features_polar)  # Experiment here with different feature sets defined in part 1
sequences = training.get_all_sequences()
Xlengths = training.get_all_Xlengths()
for word in words_to_train:
    start = timeit.default_timer()
    model = SelectorDIC(sequences, Xlengths, word, 
                    min_n_components=2, max_n_components=15, random_state = 14).select()
    end = timeit.default_timer()-start
    if model is not None:
        print("Training complete for {} with {} states with time {} seconds".format(word, model.n_components, end))
    else:
        print("Training failed for {}".format(word))
Training complete for FISH with 2 states with time 9.006627241000388 seconds
Training complete for BOOK with 14 states with time 15.507243935999213 seconds
Training complete for VEGETABLE with 12 states with time 10.453405289001239 seconds
Training complete for FUTURE with 14 states with time 14.682560229000956 seconds
Training complete for JOHN with 14 states with time 100.50340179100021 seconds

Question 2: Compare and contrast the possible advantages and disadvantages of the various model selectors implemented.

Answer 2: In this section we will talk about our 3 model selectors and how they perform. Our first method, SelectorCV, uses cross-validation using K-folds, with the number of folds being 2 or 3 depending on the length. This process will train on a subset of the data, and then test against data it has not seen before. In this way we can test how our model performs against new data, to combat overfitting. This is the validation part of Cross Validation, we validate our models performance on data it hasn't seen before. Drawbacks of this method is it requires multiple sets of training data. Some of the examples only had 1 example to train on, so splitting the data into test and train was impossible.

Our second model selection technique uses Bayesian Information Criterion or BIC. Our BIC selector works by finding the log-likelihood for a given model, multiplying our log-likelihood score L by -2 , making it a minimization problem ( we are now trying to minimize this score ), and adding a penalty parameter to it that increases with model complexity. So we take our initial score, multiply by -2 to flip it, then add a penalty based on complexity. The result is , all things remaining equal, the selector will choose the least complex model. BIC suffers from features with high dimensionality. Because of the penalty added for free parameters, it will want to converge on to a simple solution , regardless of the complexity of the problem. Other heuristics such as AIC do not suffer from this problem.

Our third and final selector is the Discriminative Information Criterion or DIC. In this technique, the score is obtained by first calculating the likelihood of our known word with our model, and then subtracting the average likelihood of all other words. The end result is a selection process that favors models that have high scores for specific words, and a low probability of being all the other words. Because of this , DIC works well with more states than fewer. For each word, we have anti-evidence of competing models that better our classification task by selecting a model that scores poorly on other predictions, and well on the target prediction.

Model Selector Unit Testing

Run the following unit tests as a sanity check on the implemented model selectors. The test simply looks for valid interfaces but is not exhaustive. However, the project should not be submitted if these tests don't pass.

In [23]:
from asl_test_model_selectors import TestSelectors
suite = unittest.TestLoader().loadTestsFromModule(TestSelectors())
unittest.TextTestRunner().run(suite)
....
----------------------------------------------------------------------
Ran 4 tests in 147.687s

OK
Out[23]:
<unittest.runner.TextTestResult run=4 errors=0 failures=0>

PART 3: Recognizer

The objective of this section is to "put it all together". Using the four feature sets created and the three model selectors, you will experiment with the models and present your results. Instead of training only five specific words as in the previous section, train the entire set with a feature set and model selector strategy.

Recognizer Tutorial

Train the full training set

The following example trains the entire set with the example features_ground and SelectorConstant features and model selector. Use this pattern for you experimentation and final submission cells.

In [24]:
# autoreload for automatically reloading changes made in my_model_selectors and my_recognizer
#%load_ext autoreload
#%autoreload 2

import my_model_selectors

def train_all_words(features, model_selector):
    training = asl.build_training(features)  # Experiment here with different feature sets defined in part 1
    sequences = training.get_all_sequences()
    Xlengths = training.get_all_Xlengths()
    model_dict = {}
    for word in training.words:
        model = model_selector(sequences, Xlengths, word, 
                        n_constant=3).select()
        model_dict[word]=model
    return model_dict

models = train_all_words(features_polar, SelectorBIC)
print("Number of word models returned = {}".format(len(models)))
Number of word models returned = 112
Load the test set

The build_test method in ASLdb is similar to the build_training method already presented, but there are a few differences:

  • the object is type SinglesData
  • the internal dictionary keys are the index of the test word rather than the word itself
  • the getter methods are get_all_sequences, get_all_Xlengths, get_item_sequences and get_item_Xlengths
In [25]:
test_set = asl.build_test(features_polar)
print("Number of test set items: {}".format(test_set.num_items))
print("Number of test set sentences: {}".format(len(test_set.sentences_index)))
Number of test set items: 178
Number of test set sentences: 40

Recognizer Implementation Submission

For the final project submission, students must implement a recognizer following guidance in the my_recognizer.py module. Experiment with the four feature sets and the three model selection methods (that's 12 possible combinations). You can add and remove cells for experimentation or run the recognizers locally in some other way during your experiments, but retain the results for your discussion. For submission, you will provide code cells of only three interesting combinations for your discussion (see questions below). At least one of these should produce a word error rate of less than 60%, i.e. WER < 0.60 .

Tip: The hmmlearn library may not be able to train or score all models. Implement try/except contructs as necessary to eliminate non-viable models from consideration.

In [35]:
from my_recognizer import recognize

features = features_ground_normal # change as needed
model_selector = SelectorDIC # change as needed

# TODO Recognize the test set and display the result with the show_errors method
models = train_all_words(features, model_selector)
test_set = asl.build_test(features)

probabilities, guesses = recognize(models, test_set)
show_errors(guesses, test_set)
**** WER = 0.5730337078651685
Total correct: 76 out of 178
Video  Recognized                                                    Correct
=====================================================================================================
    2: JOHN *HOUSE *ARRIVE                                           JOHN WRITE HOMEWORK
    7: JOHN *CAR GO CAN                                              JOHN CAN GO CAN
   12: *IX *HOUSE *JOHN *HOUSE                                       JOHN CAN GO CAN
   21: *MARY *NEW *JOHN *MARY *HOUSE *CAR *ARRIVE *FUTURE            JOHN FISH WONT EAT BUT CAN EAT CHICKEN
   25: *MARY *MARY *GO IX *LOVE                                      JOHN LIKE IX IX IX
   28: *MARY *MARY *GO IX *LOVE                                      JOHN LIKE IX IX IX
   30: JOHN LIKE *MARY *LOVE IX                                      JOHN LIKE IX IX IX
   36: MARY *NOT *YESTERDAY *GIVE *LOVE *IX                          MARY VEGETABLE KNOW IX LIKE CORN1
   40: *MARY *VISIT *GIVE2 *WHO LOVE                                 JOHN IX THINK MARY LOVE
   43: JOHN *JOHN BUY HOUSE                                          JOHN MUST BUY HOUSE
   50: *JOHN *SEE BUY CAR *CAR                                       FUTURE JOHN BUY CAR SHOULD
   54: JOHN *FUTURE *MARY BUY HOUSE                                  JOHN SHOULD NOT BUY HOUSE
   57: *MARY *VISIT VISIT *LOVE                                      JOHN DECIDE VISIT MARY
   67: *LIKE *IX *JOHN BUY HOUSE                                     JOHN FUTURE NOT BUY HOUSE
   71: JOHN *SHOULD VISIT MARY                                       JOHN WILL VISIT MARY
   74: *IX *JOHN *GIVE MARY                                          JOHN NOT VISIT MARY
   77: *LOVE BLAME MARY                                              ANN BLAME MARY
   84: *LOVE *BOX *VISIT *NEW                                        IX-1P FIND SOMETHING-ONE BOOK
   89: *WHO *FUTURE GIVE *IX IX *ARRIVE COAT                         JOHN IX GIVE MAN IX NEW COAT
   90: *GIVE3 *IX IX SOMETHING-ONE WOMAN BOOK                        JOHN GIVE IX SOMETHING-ONE WOMAN BOOK
   92: JOHN GIVE IX *IX WOMAN BOOK                                   JOHN GIVE IX SOMETHING-ONE WOMAN BOOK
  100: POSS NEW CAR BREAK-DOWN                                       POSS NEW CAR BREAK-DOWN
  105: JOHN *FRANK                                                   JOHN LEG
  107: *MARY *IX FRIEND *MARY CANDY                                  JOHN POSS FRIEND HAVE CANDY
  108: WOMAN *LOVE                                                   WOMAN ARRIVE
  113: IX CAR *CAR *SOMETHING-ONE *BOX                               IX CAR BLUE SUE BUY
  119: *PREFER *LOVE IX CAR *GO                                      SUE BUY IX CAR BLUE
  122: JOHN *GIVE1 BOOK                                              JOHN READ BOOK
  139: JOHN *ARRIVE *BOX *VISIT BOOK                                 JOHN BUY WHAT YESTERDAY BOOK
  142: JOHN BUY YESTERDAY WHAT BOOK                                  JOHN BUY YESTERDAY WHAT BOOK
  158: LOVE *MARY *NOT                                               LOVE JOHN WHO
  167: JOHN IX *VISIT LOVE *LOVE                                     JOHN IX SAY LOVE MARY
  171: JOHN *SOMETHING-ONE BLAME                                     JOHN MARY BLAME
  174: *NEW *GIVE1 GIVE1 *GIRL *CAN                                  PEOPLE GROUP GIVE1 JANA TOY
  181: *MARY ARRIVE                                                  JOHN ARRIVE
  184: *IX BOY *GIVE1 TEACHER APPLE                                  ALL BOY GIVE TEACHER APPLE
  189: *WHO *JOHN *FUTURE1 BOX                                       JOHN GIVE GIRL BOX
  193: *MARY *POSS *GO BOX                                           JOHN GIVE GIRL BOX
  199: *LOVE *ARRIVE WHO                                             LIKE CHOCOLATE WHO
  201: *SOMETHING-ONE *GIVE1 *WOMAN *WOMAN *ARRIVE HOUSE             JOHN TELL MARY IX-1P BUY HOUSE
In [28]:
from my_recognizer import recognize

features = features_polar # change as needed
model_selector = SelectorBIC # change as needed

# TODO Recognize the test set and display the result with the show_errors method
models = train_all_words(features, model_selector)
test_set = asl.build_test(features)

probabilities, guesses = recognize(models, test_set)
show_errors(guesses, test_set)
**** WER = 0.550561797752809
Total correct: 80 out of 178
Video  Recognized                                                    Correct
=====================================================================================================
    2: JOHN WRITE *NEW                                               JOHN WRITE HOMEWORK
    7: *SOMETHING-ONE *PEOPLE GO *ARRIVE                             JOHN CAN GO CAN
   12: *IX *WHAT *GO1 CAN                                            JOHN CAN GO CAN
   21: JOHN *NEW WONT *NOT *GIVE1 *TEACHER *FUTURE *WHO              JOHN FISH WONT EAT BUT CAN EAT CHICKEN
   25: JOHN LIKE IX *WHO IX                                          JOHN LIKE IX IX IX
   28: JOHN *WHO IX *WHO IX                                          JOHN LIKE IX IX IX
   30: *IX LIKE *MARY *MARY *MARY                                    JOHN LIKE IX IX IX
   36: *VISIT VEGETABLE *GIRL *GIVE *MARY *MARY                      MARY VEGETABLE KNOW IX LIKE CORN1
   40: *MARY *VISIT *FUTURE1 *JOHN *MARY                             JOHN IX THINK MARY LOVE
   43: JOHN *SHOULD BUY HOUSE                                        JOHN MUST BUY HOUSE
   50: *JOHN *SEE *STUDENT CAR *JOHN                                 FUTURE JOHN BUY CAR SHOULD
   54: JOHN SHOULD *WHO BUY HOUSE                                    JOHN SHOULD NOT BUY HOUSE
   57: *MARY *VISIT VISIT MARY                                       JOHN DECIDE VISIT MARY
   67: *IX FUTURE *MARY BUY HOUSE                                    JOHN FUTURE NOT BUY HOUSE
   71: JOHN *FINISH *GIVE1 MARY                                      JOHN WILL VISIT MARY
   74: *IX *VISIT *GIVE MARY                                         JOHN NOT VISIT MARY
   77: *JOHN *MARY MARY                                              ANN BLAME MARY
   84: *MARY *GIVE1 *GIVE1 BOOK                                      IX-1P FIND SOMETHING-ONE BOOK
   89: *GIVE *GIVE *WOMAN *WOMAN IX *ARRIVE *BREAK-DOWN              JOHN IX GIVE MAN IX NEW COAT
   90: JOHN *HAVE IX SOMETHING-ONE *VISIT BOOK                       JOHN GIVE IX SOMETHING-ONE WOMAN BOOK
   92: JOHN *WOMAN IX *WOMAN WOMAN BOOK                              JOHN GIVE IX SOMETHING-ONE WOMAN BOOK
  100: POSS NEW CAR BREAK-DOWN                                       POSS NEW CAR BREAK-DOWN
  105: JOHN *VEGETABLE                                               JOHN LEG
  107: *LIKE *IX *HAVE *GO *JANA                                     JOHN POSS FRIEND HAVE CANDY
  108: *MARY *LOVE                                                   WOMAN ARRIVE
  113: IX CAR *IX *SOMETHING-ONE *BUY1                               IX CAR BLUE SUE BUY
  119: *PREFER *BUY1 IX CAR *GO                                      SUE BUY IX CAR BLUE
  122: JOHN *GIVE1 BOOK                                              JOHN READ BOOK
  139: JOHN *BUY1 WHAT *BLAME *CHOCOLATE                             JOHN BUY WHAT YESTERDAY BOOK
  142: JOHN BUY YESTERDAY WHAT BOOK                                  JOHN BUY YESTERDAY WHAT BOOK
  158: LOVE JOHN WHO                                                 LOVE JOHN WHO
  167: *IX IX *VISIT LOVE MARY                                       JOHN IX SAY LOVE MARY
  171: *MARY *SOMETHING-ONE BLAME                                    JOHN MARY BLAME
  174: *JOHN *GIVE3 GIVE1 *YESTERDAY *JOHN                           PEOPLE GROUP GIVE1 JANA TOY
  181: *EAT ARRIVE                                                   JOHN ARRIVE
  184: ALL BOY *GIVE1 TEACHER APPLE                                  ALL BOY GIVE TEACHER APPLE
  189: *MARY *VISIT *VISIT BOX                                       JOHN GIVE GIRL BOX
  193: JOHN *POSS *VISIT BOX                                         JOHN GIVE GIRL BOX
  199: *JOHN *VIDEOTAPE WHO                                          LIKE CHOCOLATE WHO
  201: JOHN *MAN *MAN *IX BUY HOUSE                                  JOHN TELL MARY IX-1P BUY HOUSE
In [34]:
from my_recognizer import recognize

features = features_polar # change as needed
model_selector = SelectorCV # change as needed

# TODO Recognize the test set and display the result with the show_errors method
models = train_all_words(features, model_selector)
test_set = asl.build_test(features)

probabilities, guesses = recognize(models, test_set)
show_errors(guesses, test_set)
**** WER = 0.5955056179775281
Total correct: 72 out of 178
Video  Recognized                                                    Correct
=====================================================================================================
    2: JOHN *BOOK *GO                                                JOHN WRITE HOMEWORK
    7: JOHN *HAVE GO CAN                                             JOHN CAN GO CAN
   12: JOHN *WHAT *WHAT CAN                                          JOHN CAN GO CAN
   21: JOHN *NEW WONT *WHO *CAR *CAR *VISIT *WHO                     JOHN FISH WONT EAT BUT CAN EAT CHICKEN
   25: JOHN *MARY *MARY *MARY *LOVE                                  JOHN LIKE IX IX IX
   28: JOHN *MARY *MARY *MARY *LOVE                                  JOHN LIKE IX IX IX
   30: *SHOULD LIKE *MARY *MARY *GO                                  JOHN LIKE IX IX IX
   36: *IX *PREFER *GIRL *GIVE *MARY *JOHN                           MARY VEGETABLE KNOW IX LIKE CORN1
   40: JOHN *GIVE *APPLE MARY *MARY                                  JOHN IX THINK MARY LOVE
   43: JOHN *IX BUY HOUSE                                            JOHN MUST BUY HOUSE
   50: *JOHN *SEE BUY CAR *WHAT                                      FUTURE JOHN BUY CAR SHOULD
   54: JOHN SHOULD *MARY BUY HOUSE                                   JOHN SHOULD NOT BUY HOUSE
   57: *IX *MARY *IX MARY                                            JOHN DECIDE VISIT MARY
   67: JOHN *YESTERDAY NOT BUY HOUSE                                 JOHN FUTURE NOT BUY HOUSE
   71: JOHN *FINISH *GO MARY                                         JOHN WILL VISIT MARY
   74: *GO *IX *GO *GO                                               JOHN NOT VISIT MARY
   77: *JOHN BLAME *LOVE                                             ANN BLAME MARY
   84: *LOVE *GIVE1 *GO BOOK                                         IX-1P FIND SOMETHING-ONE BOOK
   89: *MARY *GIVE *WOMAN *WOMAN IX *ARRIVE *BREAK-DOWN              JOHN IX GIVE MAN IX NEW COAT
   90: JOHN *GIVE1 *WOMAN SOMETHING-ONE *GIVE1 *ARRIVE               JOHN GIVE IX SOMETHING-ONE WOMAN BOOK
   92: JOHN *WOMAN IX *WOMAN WOMAN BOOK                              JOHN GIVE IX SOMETHING-ONE WOMAN BOOK
  100: POSS NEW CAR BREAK-DOWN                                       POSS NEW CAR BREAK-DOWN
  105: JOHN *POSS                                                    JOHN LEG
  107: *LIKE *SOMETHING-ONE *HAVE *GO *MARY                          JOHN POSS FRIEND HAVE CANDY
  108: WOMAN *LOVE                                                   WOMAN ARRIVE
  113: *JOHN *HAVE BLUE SUE *BOX                                     IX CAR BLUE SUE BUY
  119: *VEGETABLE *BUY1 *GO *HAVE *APPLE                             SUE BUY IX CAR BLUE
  122: JOHN *HOUSE BOOK                                              JOHN READ BOOK
  139: JOHN *BUY1 *DECIDE YESTERDAY *ARRIVE                          JOHN BUY WHAT YESTERDAY BOOK
  142: JOHN BUY YESTERDAY WHAT BOOK                                  JOHN BUY YESTERDAY WHAT BOOK
  158: LOVE *VISIT WHO                                               LOVE JOHN WHO
  167: JOHN *SOMETHING-ONE *MARY *WOMAN *LOVE                        JOHN IX SAY LOVE MARY
  171: JOHN *SUE BLAME                                               JOHN MARY BLAME
  174: *HAVE *GIVE3 GIVE1 *GIRL *WHAT                                PEOPLE GROUP GIVE1 JANA TOY
  181: *SUE *BOX                                                     JOHN ARRIVE
  184: *SOMETHING-ONE BOY *BLAME TEACHER APPLE                       ALL BOY GIVE TEACHER APPLE
  189: JOHN *SOMETHING-ONE *YESTERDAY BOX                            JOHN GIVE GIRL BOX
  193: JOHN *GIVE1 *YESTERDAY BOX                                    JOHN GIVE GIRL BOX
  199: *LOVE CHOCOLATE *TELL                                         LIKE CHOCOLATE WHO
  201: JOHN *MARY *WOMAN *WOMAN BUY HOUSE                            JOHN TELL MARY IX-1P BUY HOUSE
In [38]:
# Results for below
from collections import defaultdict
results = defaultdict(dict)

results['DIC']['GROUND_NORMAL'] = """0.5274157303370787"""
results['DIC']['NORM'] = """0.6460674157303371"""
results['DIC']['POLAR'] = """0.5680898876404494"""
results['DIC']['DELTA'] = """0.6460674157303371"""

results["CV"]["NORM"] = """0.6235955056179775"""
results["CV"]["POLAR"] = """0.6453188465218732"""
results["CV"]["DELTA"] = """0.6840310687510874"""
results["CV"]["GROUND"] = """0.631561081984251"""
pd.DataFrame(results)

results["BIC"]["GROUND_NORMAL"] = """0.601123595505618"""
results["BIC"]["POLAR"] = """0.550561797752809"""
results["BIC"]["DELTA"] = """0.6235955056179775"""

print("WER Table")
df = pd.DataFrame(results)
df
WER Table
Out[38]:
BIC CV DIC
DELTA 0.6235955056179775 0.6840310687510874 0.6460674157303371
GROUND NaN 0.631561081984251 NaN
GROUND_NORMAL 0.601123595505618 NaN 0.5274157303370787
NORM NaN 0.6235955056179775 0.6460674157303371
POLAR 0.550561797752809 0.6453188465218732 0.5680898876404494

Question 3: Summarize the error results from three combinations of features and model selectors. What was the "best" combination and why? What additional information might we use to improve our WER? For more insight on improving WER, take a look at the introduction to Part 4.

Answer 3:

As we see in our WER table above, the SelectorDIC model selector with ground_normal feature set has proven to have the least word error rate on our final test with just under 53% WER. Given how different people are, difference in height, arm span, and other inconsistencies, I have chosen to use ground normal feature set, which measures the distance from 'ground' which in our case is the nose, and then normalized to bring all those numbers closer together.

In order to improve our WER, we have a few options. We could gather more data and train on it. This would allow more cross-validation options , as our chance of over-fitting would increase with the amount of data we train on ( without some sort of validation ).

We could also switch to a n-gram model ( such as bi-gram or tri-gram ). This process involves utilizing the fact the words often appear in sequences, and you can get much better probability by predicting sets of 3 words in sequence rather than just 1 word, like the 0 gram approach we have taken above. You could even extend this to 11-gram sequences, but research has been done that most of the english language can work by breaking it up into smaller pieces.

We have made great steps in our ASL Recognizer, but still have far way to go to get to some usable prediction rates. The n-gram approach looks the most promising, and combined with getting more data to train on could yield some pretty powerful results.

Recognizer Unit Tests

Run the following unit tests as a sanity check on the defined recognizer. The test simply looks for some valid values but is not exhaustive. However, the project should not be submitted if these tests don't pass.

In [39]:
from asl_test_recognizer import TestRecognize
suite = unittest.TestLoader().loadTestsFromModule(TestRecognize())
unittest.TextTestRunner().run(suite)
..
{'JOHN': -103.26480406067158, 'WRITE': -195.47529328005996, 'HOMEWORK': -114.32341220258823, 'IX-1P': -116.18987181689933, 'SEE': -96.471847665118261, 'YESTERDAY': -115.63411722775028, 'IX': -121.7612768710008, 'LOVE': -254.61574832720669, 'MARY': -117.05739027932468, 'CAN': -127.46932225652631, 'GO': -134.6699957358376, 'GO1': -327.27884206941246, 'FUTURE': -113.83102945098159, 'GO2': -65081.724421020037, 'PARTY': -1700.2330313050265, 'FUTURE1': -825.45992617672334, 'HIT': -531.32309070590293, 'BLAME': -166.79201251150431, 'FRED': -2040.4542582777069, 'FISH': -113074.88580804384, 'WONT': -131.41405687374379, 'EAT': -209.16288856040291, 'BUT': -467.14823507497943, 'CHICKEN': -409.36840106813929, 'VEGETABLE': -273.24701946136673, 'CHINA': -1226.2386639231354, 'PEOPLE': -249.25900909944701, 'PREFER': -134.15933913420719, 'BROCCOLI': -347.49289638841685, 'LIKE': -141.21254558960075, 'LEAVE': -324.33875033401785, 'SAY': -805.51867423732415, 'BUY': -201.11831549864672, 'HOUSE': -112.54168784044693, 'KNOW': -535.71999854701596, 'CORN': -147.72526677393745, 'CORN1': -174.337617422531, 'THINK': -516.24879591948388, 'NOT': -98.669258189036256, 'PAST': -759.77052432640835, 'LIVE': -2339.2512208089588, 'CHICAGO': -280.68331656359521, 'CAR': -111.97605899905938, 'SHOULD': -129.45689986833722, 'DECIDE': -444.49338309872905, 'VISIT': -110.19970372990471, 'MOVIE': -718.70574798745042, 'WANT': -2380.8563994615056, 'SELL': -516.39664549068311, 'TOMORROW': -215.71122445650417, 'NEXT-WEEK': -468.42864676787781, 'NEW-YORK': -238.06161551728925, 'LAST-WEEK': -190.06387445576678, 'WILL': -911.61036236071016, 'FINISH': -173.138822924561, 'ANN': -229.68862794548232, 'READ': -170.52418508702414, 'BOOK': -345.86473095597279, 'CHOCOLATE': -662.0774311750996, 'FIND': -inf, 'SOMETHING-ONE': -116.79618624346568, 'POSS': -109.22390639267745, 'BROTHER': -165.06206541109862, 'ARRIVE': -129.2419483685234, 'HERE': -2983.1943463294319, 'GIVE': -118.00870765190047, 'MAN': -184.33499562958639, 'NEW': -127.15978664114473, 'COAT': -177.81495484802531, 'WOMAN': -162.70824770481366, 'GIVE1': -108.97042786099264, 'HAVE': -253.99772642681586, 'FRANK': -123.37855537163139, 'BREAK-DOWN': -167.03602239049252, 'SEARCH-FOR': -413.98265313223516, 'WHO': -116.57864581749671, 'WHAT': -162.31937987316144, 'LEG': -inf, 'FRIEND': -7287.5069148617395, 'CANDY': -inf, 'BLUE': -878.60342824907275, 'SUE': -174.29990787967023, 'BUY1': -365.22382324085328, 'STOLEN': -993.42858730384057, 'OLD': -inf, 'STUDENT': -107.45768319896899, 'VIDEOTAPE': -234.28153927922079, 'BORROW': -inf, 'MOTHER': -672.66850088142235, 'POTATO': -1832.745673227153, 'TELL': -114.79857729397237, 'BILL': -309.46385324889809, 'THROW': -127.03782309246401, 'APPLE': -136.27108945345938, 'NAME': -3211545.5896422071, 'SHOOT': -178.97470226590931, 'SAY-1P': -2138.7987868200107, 'SELF': -770.48936117915684, 'GROUP': -1208.4516201110762, 'JANA': -372.76801972535424, 'TOY1': -523.0626118521011, 'MANY': -2392.4758804781086, 'TOY': -1224.2525071633206, 'ALL': -228.14057946214541, 'BOY': -398.24095194056025, 'TEACHER': -109.63013326494548, 'GIRL': -176.827352285004, 'BOX': -192.03088703591339, 'GIVE2': -257.06963404385687, 'GIVE3': -112.30430654941468, 'GET': -254.57246285754462, 'PUTASIDE': -1395.2816671750966}
----------------------------------------------------------------------
Ran 2 tests in 121.244s

OK
Out[39]:
<unittest.runner.TextTestResult run=2 errors=0 failures=0>

PART 4: (OPTIONAL) Improve the WER with Language Models

We've squeezed just about as much as we can out of the model and still only get about 50% of the words right! Surely we can do better than that. Probability to the rescue again in the form of statistical language models (SLM). The basic idea is that each word has some probability of occurrence within the set, and some probability that it is adjacent to specific other words. We can use that additional information to make better choices.

Additional reading and resources
Optional challenge

The recognizer you implemented in Part 3 is equivalent to a "0-gram" SLM. Improve the WER with the SLM data provided with the data set in the link above using "1-gram", "2-gram", and/or "3-gram" statistics. The probabilities data you've already calculated will be useful and can be turned into a pandas DataFrame if desired (see next cell).
Good luck! Share your results with the class!

In [ ]:
# create a DataFrame of log likelihoods for the test word items
df_probs = pd.DataFrame(data=probabilities)
df_probs.head()