$29
Since working on part 1 of the project, you've learned many
approaches that will allow us to improve both the design of
data/functions, as well as add new functionality!
== Data/Function Design ==
- You'll enhance each flash card to support an arbitrary
number of "tags" (i.e., string labels).
- You'll generalize the meaning of a deck, such as to be
agnostic as to the very meaning of cards (and thus
support a wider variety of decks).
- You'll enhance the menu system to be re-usable, as
well as to support quitting (i.e., leave without forcing a
selection).
== Application Features ==
- You'll implement a second method for interpreting
self-reported correctness of a card, this time using
some machine learning (ML) to process natural language (NLP);
the user will be able to select which method to use (since
both methods have their tradeoffs!).
- When a user doesn't get a card correct (via self-report),
that card is placed at the back of the deck; thus, a deck
is only completed when a user gets all cards correct.
- You'll provide deck options that are a subset of cards
containing a particular tag (e.g., all "hard" cards, or
those in the topic of "science").
- Once the program is run, the user will be able to study
as many decks as they wish, selecting subsequent decks
from the menu until they quit.
Of course, we'll design this program step-by-step :)
When designing this enhanced project, you are welcome to draw
upon your project part 1, our sample solutions (for part 1, and
any homework), and/or lecture notes as you see fit & helpful.
Lastly, here are a few overall project requirements...
- Now that mutation has been covered, you may use it (unless
otherwise stated in the instructions); however, your usage
will be evaluated based upon the guidelines from class.
- As included in the instructions, all interactive parts of
this program MUST make effective use of the reactConsole
framework.
- Staying consistent with our Style Guide...
* All functions must have:
a) a preceding comment specifying what it does
b) an associated @EnabledTest function with sufficient
tests using testSame
* All data must have:
a) a preceding comment specifying what it represents
b) associated representative examples
c) for classes with member functions, an associated
@EnabledTest function with sufficient tests for all
the member functions of the class
- You will be evaluated on a number of criteria, including...
* Adherence to instructions and the Style Guide
* Correctly producing the functionality of the program
* Design decisions that include choice of tests, appropriate
application of programming approaches (e.g., sequence
abstractions, recursion, mutation), and task/type-driven
decomposition of functions.
-----------------------------------------------------------------
Flash Card data design
(Hint: see Homework 5, Problem 3)
-----------------------------------------------------------------
TODO 1/1: Design the data type TaggedFlashCard to represent a
single flash card.
You should be able to represent the text prompt on
the front of the card, the text answer on the back,
as well as any number of textual tags (such as "hard"
or "science" -- this shouldn't come from any fixed
set of options, but truly open to however someone
wishes to categorize their cards).
Each card should have two member functions:
- isTagged, which determines if the card has a
supplied tag (e.g., has this card been tagged
as "hard"?)
- fileFormat, which produces a textual representation
of the card as "front|back|tag1,tag2,..."; that is
all three parts of the card separated with the pipe
('|') character, and further separate any tags with
a comma
Include *at least* 3 example cards (which will come
in handy later for tests!), and make sure to test
the required member functions.
(just useful values for
the separation characters)
val sepCard = "|"
val sepTag = ","
-----------------------------------------------------------------
Files of tagged flash cards
-----------------------------------------------------------------
Now that we have our updated cards, let's update how we read
them from files.
TODO 1/2: Design the function stringToTaggedFlashCard that
takes a string, assumed to be in the format described
for the fileFormat member function above, and produces
the corresponding tagged flash card.
Hint: review part 1 of the project, TODO 2/3
TODO 2/2: Design the function readTaggedFlashCardsFile that
takes a path to a file and produces a list of
tagged flash cards.
If the file does not exist, return an empty list.
Otherwise, you can assume that every line is
formatted in the string format we just worked with.
Hint:
- Review part 1 of the project, TODO 3/3
- We've provided an "example_tagged.txt" file that you
can use for testing if you'd like; also make sure to
test your function when the supplied file does not
exist!
-----------------------------------------------------------------
Deck design
-----------------------------------------------------------------
If you think about it, once a deck has been selected, our study
application doesn't need much information about cards to work...
in fact, it doesn't even need the concept of a card. Consider
the following:
The deck is either exhausted,
showing the question, or
showing the answer
enum class DeckState {
EXHAUSTED,
QUESTION,
ANSWER,
}
Basic functionality of any deck
interface IDeck {
The state of the deck
fun getState(): DeckState
The currently visible text
(or null if exhausted)
fun getText(): String?
The number of question/answer pairs
(does not change when question are
cycled to the end of the deck)
fun getSize(): Int
Shifts from question -> answer
(if not QUESTION state, returns the same IDeck)
fun flip(): IDeck
Shifts from answer -> next question (or exhaustion);
if the current question was correct it is discarded,
otherwise cycled to the end of the deck
(if not ANSWER state, returns the same IDeck)
fun next(correct: Boolean): IDeck
}
This contract of operations will allow our study application to
work with a variety of sources, including lists and even code
that never explicitly stores cards!
(For a similar problem, see Homework 6, Problem 3, TODO 2,
where you implemented stateful classes to integrate with an
object-oriented reactConsole.)
TODO 1/2: Design TFCListDeck to implement the IDeck interface
for a supplied list of tagged flash cards. For this
problem your class must have *no* mutable state and
all member data should be private.
When testing, make sure to test the behavior of all
the member functions of the interface in a variety
of situations.
Hint: using default arguments can make your class
easier to create initially, see...
kotlinlang.org/docs/functions.html#default-arguments
TODO 2/2: Now design PerfectSquaresDeck to implement the IDeck
interface. You are *not* allowed to generate any
flash cards, nor have mutable state; the goal is to
act as though it had a list produced by the
perfectSquares function in part 1 of the project,
but without ever having to generate all those cards!
Again, as is generally good practice, keep all your
member data private!
Hint: you will still need to keep track of the
*sequence* of upcoming numbers (particularly
as some may get cycled back due to incorrect
responses).
-----------------------------------------------------------------
Menu design
-----------------------------------------------------------------
The chooseOption function in part 1 of the project was good, but
let's see what we can do to improve upon it in two core ways...
a) Part 1 allowed you to select from amongst decks, which means
you'd have to copy-paste if you wanted to have a menu of
other data (such as files, or months of the year); let's
make the function agnostic as to the type of the list items
being selected.
b) Part 1 didn't allow for the possibility of not selecting an
option; let's add a quit feature!
To help with (a), consider the following interface, which
requires that a menu option be able to return a textual
representation (that is then displayed in the menu!)...
the only required capability for a menu option
is to be able to render a title
interface IMenuOption {
fun menuTitle(): String
}
as well as the following general implementation (great for
tests & examples), which satisfies the contract via pairing
a value (of any type) with a name...
a menu option with a single value and name
data class NamedMenuOption<T>(val option: T, val name: String) : IMenuOption {
override fun menuTitle(): String = name
}
individual examples, as well as a list
(an example for a list of menu options!)
val opt1A = NamedMenuOption(1, "apple")
val opt2B = NamedMenuOption(2, "banana")
val optsExample = listOf(opt1A, opt2B)
TODO 1/1: Finish designing the program chooseMenuOption that
takes a list (assumed to be non-empty) of any type
(as long as it implements the IMenuOption interface),
produces a corresponding numbered menu (1-# of list
items, each showing its menuTitle), and returns the
list item corresponding to the number entered (or null
if 0 was entered to indicate a desire to quit without
choosing an option). Keep displaying the menu until a
valid menu selection (or quitting) is indicated.
Hints:
- You'll find the code from chooseOption (in part 1)
to be a *very* good starting point.
- Homework 5, Problem 4, has a very similar interface,
which can give you an idea for how you'd use it.
- To help you get started, you have some examples
above and prompts below; a "stub" for the
chooseMenuOption function (to help with the
signature and overall structure); and a set of
tests that should pass once the program has been
completed.
Some useful outputs
val menuPrompt = "Enter your choice (or 0 to quit)"
val menuQuit = "You quit"
val menuChoicePrefix = "You chose: "
Provides an interactive opportunity for the user to choose
an option or quit.
fun <T : IMenuOption> chooseMenuOption(options: List<T>): T? {
your code here!
- call reactConsole (with appropriate handlers)
- return the selected option (or null for quit)
}
@EnabledTest
fun testChooseMenuOption() {
testSame(
captureResults(
{ chooseMenuOption(listOf(opt1A)) },
"howdy",
"0",
),
CapturedResult(
null,
"1. ${opt1A.name}",
"",
menuPrompt,
"1. ${opt1A.name}",
"",
menuPrompt,
menuQuit,
),
"quit",
)
testSame(
captureResults(
{ chooseMenuOption(optsExample) },
"hello",
"10",
"-3",
"1",
),
CapturedResult(
opt1A,
"1. ${opt1A.name}", "2. ${opt2B.name}", "", menuPrompt,
"1. ${opt1A.name}", "2. ${opt2B.name}", "", menuPrompt,
"1. ${opt1A.name}", "2. ${opt2B.name}", "", menuPrompt,
"1. ${opt1A.name}", "2. ${opt2B.name}", "", menuPrompt,
"${menuChoicePrefix}${opt1A.name}",
),
"1",
)
testSame(
captureResults(
{ chooseMenuOption(optsExample) },
"3",
"-1",
"2",
),
CapturedResult(
opt2B,
"1. ${opt1A.name}", "2. ${opt2B.name}", "", menuPrompt,
"1. ${opt1A.name}", "2. ${opt2B.name}", "", menuPrompt,
"1. ${opt1A.name}", "2. ${opt2B.name}", "", menuPrompt,
"${menuChoicePrefix}${opt2B.name}",
),
"2",
)
}
-----------------------------------------------------------------
Machine learning for sentiment analysis
-----------------------------------------------------------------
In part 1 of the project, you designed isPositive as a way to
interpret whether a student's self-report was positive or
negative; in the world of Machine Learning (a subfield of
Artificial Intelligence, or AI), this is an approach to
"sentiment analysis" - a problem in Natural Language Processing
(NLP) that seeks to analyze text to understand the emotional
tone of some text.
In this context, what you built was a "binary classifier" of
text, meaning it output one of two values according to the input
string. In Kotlin we can describe this input-output relationship
using the following shortcut...
typealias PositivityClassifier = (String) -> Boolean
This code simply means we can now use PositivityClassifier
anywhere we would have used the type on the right (e.g.,
as the type in a function's parameter or return type).
Our goal is now to try and use a more sophisticated approach
to sentiment analysis - one that learns positivity/negativity
based upon a dataset of supplied examples. To represent such a
dataset, consider the following type...
data class LabeledExample<E, L>(val example: E, val label: L)
This associates a "label" (such as positive vs negative, or
cat video vs boring) with an example. Here is one such dataset:
val datasetYN: List<LabeledExample<String, Boolean>> =
listOf(
LabeledExample("yes", true),
LabeledExample("y", true),
LabeledExample("indeed", true),
LabeledExample("aye", true),
LabeledExample("oh yes", true),
LabeledExample("affirmative", true),
LabeledExample("roger", true),
LabeledExample("uh huh", true),
LabeledExample("true", true),
just a visual separation of
the positive/negative examples
LabeledExample("no", false),
LabeledExample("n", false),
LabeledExample("nope", false),
LabeledExample("negative", false),
LabeledExample("nay", false),
LabeledExample("negatory", false),
LabeledExample("uh uh", false),
LabeledExample("absolutely not", false),
LabeledExample("false", false),
)
FYI: we call this dataset "balanced" since it has an equal
number of examples of the labels (i.e., # true and #false).
Such a balance is *one* tool (of many) when trying to avoid
algorithmic bias (en.wikipedia.org/wiki/Algorithmic_bias).
Notice that our simple heuristic of the first letter is pretty
good according to this dataset, but will make some lucky
guesses (e.g., "false") and some actual mistakes (e.g., "true").
We have provided below that code, as well as a set of tests that
reference our labeled dataset - make sure you understand all of
this code (including the comments in the tests about when & how
the heuristic is predictably getting the answer wrong).
Heuristically determines if the supplied string
is positive based upon the first letter being Y
fun isPositiveSimple(s: String): Boolean {
return s.uppercase().startsWith("Y")
}
tests that an element of the dataset matches
with expectation of its correctness on a
particular classifier
fun helpTestElement(
index: Int,
expectedIsCorrect: Boolean,
isPos: PositivityClassifier,
) {
testSame(
isPos(datasetYN[index].example),
when (expectedIsCorrect) {
true -> datasetYN[index].label
false -> !datasetYN[index].label
},
when (expectedIsCorrect) {
true -> datasetYN[index].example
false -> "${ datasetYN[index].example } <- WRONG"
},
)
}
@EnabledTest
fun testIsPositiveSimple() {
val classifier = ::isPositiveSimple
correctly responds with positive
for (i in 0..1) {
helpTestElement(i, true, classifier)
}
incorrectly responds with negative
for (i in 2..8) {
helpTestElement(i, false, classifier)
}
correctly responds with negative, sometimes
due to luck (i.e., anything not starting
with the letter Y is assumed negative)
for (i in 9..17) {
helpTestElement(i, true, classifier)
}
}
One approach we *could* take is just to have the computer learn
by rote memorization: that is, respond with the labeled answer
from the dataset. But what about if the student supplies an
input not in this list? The approach we'll try as a way to
handle this situation is the following...
- If the response is known in the dataset (independent of
upper/lower-case), use the associated label
- Otherwise...
Find the 3 "closest" examples and respond with a majority
vote of their associated labels
This algorithm will represent our attempt to "generalize"
from the dataset; we know we'll always get certain responses
correct, and we'll let our dataset inform the response of
unknown inputs. As with all approaches based upon machine
learning, this approach is likely to make mistakes (even those
that we'll find confusing/comical), and so we should be
judicious in how we apply the system in the world.
Now let's build up this classifier, step-by-step :)
TODO 1/5: When finding closest examples, and majority vote, it
will be helpful to be able to get the "top-k" of a
list by some measure; meaning, a function that can
get the top-3 strings in a list by length, but
equally identify the top-1 (i.e., best) song by
ratings. To help, consider the following definition
of an "evaluation" function: one that takes an input
of some type and associates an output "score" (where
bigger scores are understood to be better):
typealias EvaluationFunction<T> = (T) -> Int
Design the function topK that takes a list of
items, k (assumed to be a postive integer), and a
corresponding evaluation function, and then returns
the k items in the list that get the highest score
(if there are ties, you are free to return any of the
winners; if there aren't enough items in the list,
return as many as you can).
Hint: You did this problem in Homework 7, Problem 1
- To simplify, you can avoid the ItemScore type
by using the built-in `zip` function that you
implemented in Homework 7, Problem 3.
- Later functions will use topK and assume the
parameter ordering is as described above (which
is a small swap from the sample solution).
TODO 2/5: Great! Now we have to answer the question from before:
what does it mean for two strings to be "close"?
There are actually multiple reasonable ways of
capturing such a distance, one of which is the
Levenshtein Distance, which describes the minimum
number of single-character changes (e.g., adding a
character, removing one, or substituting) required to
change one sequence into another
(https: en.wikipedia.org/wiki/Levenshtein_distance).
Your task is to design the function
levenshteinDistance that computes this distance for
two supplied strings.
Hint: Homework 7, Problem 2 :)
TODO 3/5: Great! Now let's design a "k-Nearest Neighbor"
classifier (you can read online description, such as
on Wikipedia, for lots of details & variants, but
we'll give you all the information you need here).
The goal here: given a dataset of labeled examples,
a distance function, and a number k, let the k
closest elements of the dataset "vote" (with their
label) as to what the label of a new element
should be. To be clear, here is a way of describing
a distance function, producing a integer distance
between two elements of a type...
typealias DistanceFunction<T> = (T, T) -> Int
Since this method might give an incorrect response,
we'll return not only predicted label, but the number
of "votes" received for that label (out of k)...
data class ResultWithVotes<L>(val label: L, val votes: Int)
Your task is to uncomment and then *test* the supplied
nnLabel function (note: you might need to fix up the
ordering of your topK arguments to play nicely with
the code here - you should NOT change this function).
You'll find guiding comments to help.
uses k-nearest-neighbor (kNN) to predict the label
for a supplied example given a labeled dataset
and distance function
fun <E, L> nnLabel(
queryExample: E,
dataset: List<LabeledExample<E, L>>,
distFunc: DistanceFunction<E>,
k: Int,
): ResultWithVotes<L> {
1. Use topK to find the k-closest dataset elements:
finding the elements whose negated distance is the
greatest is the same as finding those that are closest.
val closestK =
topK(dataset, k) {
-distFunc(queryExample, it.example)
}
2. Discard the examples, we only care about their labels
val closestKLabels = closestK.map { it.label }
3. For each distinct label, count up how many time it
showed up in step #2
(Note: once we know the Map type, there are WAY simpler
ways to do this!)
val labelsWithCounts =
closestKLabels.distinct().map {
label ->
Pair(
first = label
label,
second = number of votes
closestKLabels.filter({ it == label }).size,
)
}
4. Use topK to get the label with the greatest count
val topLabelWithCount = topK(labelsWithCounts, 1, { it.second })[0]
5. Return both the label and the number of votes (of k)
return ResultWithVotes(
topLabelWithCount.first,
topLabelWithCount.second,
)
}
@EnabledTest
fun testNNLabel() {
don't change this dataset:
think of them as points on a line...
(with ? referring to the example below)
a a ? b b
|--- --- --- --- --- --- --- --- --- ---|
1 2 3 4 5 6 7 8 9 10
val dataset =
listOf(
LabeledExample(2, "a"),
LabeledExample(3, "a"),
LabeledExample(7, "b"),
LabeledExample(10, "b"),
)
A simple distance: just the absolute value
fun myAbsVal(
a: Int,
b: Int,
): Int {
val diff = a - b
return when (diff >= 0) {
true -> diff
false -> -diff
}
}
TODO: to demonstrate that you understand how kNN is
supposed to work (and what the supplied code returns),
you are going to write tests here for a selection of
cases that use the dataset and distance function above.
To help you get started, consider testing for point 5,
with k=3:
a) All the points with their distances are...
a = |2 - 5| = 3
a = |3 - 5| = 3
b = |7 - 5| = 2
b = |10 - 5| = 5
b) SO, the labels of the three closest are...
a (2 votes)
b (1 vote)
c) SO, kNN in this situation would predict the label
for this point to be "a", with confidence 2/3 (medium)
We capture this test as...
testSame(
nnLabel(5, dataset, ::myAbsVal, k = 3),
ResultWithVotes("a", 2),
"NN: 5->a, 2/3",
medium confidence
)
Now your task is to write tests for the following
additional cases...
1. 1 (k=1)
2. 1 (k=2)
3. 10 (k=1)
4. 10 (k=2)
}
TODO 4/5: Ok - now it's time to put some pieces together!!
Finish designing the function yesNoClassifier below -
you've been provided with guiding steps, as well as
tests that should pass, including those that are
incorrect (with lots of confidence!).
we'll generally use k=3 in our classifier
val classifierK = 3
fun yesNoClassifier(s: String): ResultWithVotes<Boolean> {
1. Convert the input to lowercase
(since) the data set is all lowercase
2. Check to see if the lower-case input
shows up exactly within the dataset
(you can assume there are no duplicates)
3. If the input was found, simply return its label with 100%
confidence (3/3); otherwise, return the result of
performing a 3-NN classification using the dataset and
Levenshtein distance metric.
}
@EnabledTest
fun testYesNoClassifier() {
testSame(
yesNoClassifier("YES"),
ResultWithVotes(true, 3),
"YES: 3/3",
)
testSame(
yesNoClassifier("no"),
ResultWithVotes(false, 3),
"no: 3/3",
)
testSame(
yesNoClassifier("nadda"),
ResultWithVotes(false, 2),
"nadda: 2/3",
) pretty good ML!
testSame(
yesNoClassifier("yerp"),
ResultWithVotes(true, 3),
"yerp: 3/3",
) pretty good ML!
testSame(
yesNoClassifier("ouch"),
ResultWithVotes(true, 3),
"ouch: 3/3",
) seems very confident in this wrong answer...
testSame(
yesNoClassifier("now"),
ResultWithVotes(false, 3),
"now 3/3",
) seems very confident, given the input doesn't make sense?
}
TODO 5/5: Now that you have a sense of how this approach works,
including some of the (confident) mistakes it can make,
uncomment the following lines to have a classifier
(that we could use side-by-side with our heuristic).
fun isPositiveML(s: String): Boolean = yesNoClassifier(s).label
@EnabledTest
fun testIsPositiveML() {
correctly responds with positive (rote memorization)
for (i in 0..8) {
helpTestElement(i, true, ::isPositiveML)
}
correctly responds with negative (rote memorization)
for (i in 9..17) {
helpTestElement(i, true, ::isPositiveML)
}
}
-----------------------------------------------------------------
Final app!
-----------------------------------------------------------------
Whew! You've done a lot :)
Now let's put it together and study!!
TODO 1/2: Design the program studyDeck2 that uses the
reactConsole function to study through a
supplied deck using a supplied classifier to
interpret self-reported correctness.
The program should produce the following data:
represents the result of a study session:
how many questions were originally in the deck,
how many total attempts were required to get
them all correct!
data class StudyDeckResult(val numQuestions: Int, val numAttempts: Int)
Look back to the process you followed for studyDeck in
part 1 of the project: you'll first want to design a
state type, then build the main reactConsole function,
and finally design all the handlers (and don't forget
to test ALL functions, including the program!).
In case it helps, here's a trace of a short example
study session (using the simple classifier), with
notes indicated by "<--"
What is the capital of Massachusetts, USA?
Think of the result? Press enter to continue
<-- user just pressed enter, so ""
Boston
Correct? (Y)es/(N)o
yup
What is the capital of California, USA?
Think of the result? Press enter to continue
Sacramento
Correct? (Y)es/(N)o
no :( <-- cycles Cali to the back!
What is the capital of the United Kingdom?
Think of the result? Press enter to continue
London
Correct? (Y)es/(N)o
YES!
What is the capital of California, USA?
Think of the result? Press enter to continue
Sacramento
Correct? (Y)es/(N)o
yessir!
Questions: 3, Attempts: 4 <-- useful summary of return
Some useful prompts
val studyThink = "Think of the result? Press enter to continue"
val studyCheck = "Correct? (Y)es/(N)o"
TODO 2/2: Finally, design the program study2 that...
a) Uses chooseMenuOption to select from amongst a
list of decks; the options must include at least
one deck read from a file (using
readTaggedFlashCardsFile), one generated by code
(using PerfectSquaresDeck), and one that filters
based upon a tag being present (e.g., only
"hard" cards from a list; this may be the cards
read from a file).
b) If the menu in (a) didn't result in quitting, then
uses chooseMenuOption again to select from amongst
the two sentiment analysis functions.
c) If the menu in (b) didn't result in quitting, then
uses studyDeck2 to study through the selected deck
with the selected sentiment analysis function.
d) Returns to (a) and continues until either of the
two menus indicate a desire to quit.
Make sure to provide tests that capture (at least)...
- Quitting at the selection of decks
- Quitting at the selection of sentiment analysis
functions
- Studying through at least one deck
some useful labels
val optSimple = "Simple Self-Report Evaluation"
val optML = "ML Self-Report Evaluation"
-----------------------------------------------------------------
fun main() {
}
runEnabledTests(this)
main()