Skip to content

Commit

Permalink
Make rules.json useable in both frontend and backend
Browse files Browse the repository at this point in the history
- deprecate hard/soft rules from backend
- replace 'fuzzy rule' teminology
  • Loading branch information
wxyxinyu committed May 11, 2016
1 parent 500861c commit ee90b0c
Show file tree
Hide file tree
Showing 43 changed files with 358 additions and 1,279 deletions.
10 changes: 5 additions & 5 deletions core/controllers/editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@
from core.domain import fs_domain
from core.domain import gadget_registry
from core.domain import interaction_registry
from core.domain import obj_services
from core.domain import rights_manager
from core.domain import rte_component_registry
from core.domain import rule_domain
from core.domain import stats_services
from core.domain import user_services
from core.domain import value_generators_domain
Expand Down Expand Up @@ -204,7 +204,7 @@ def get(self, exploration_id):
'GADGET_SPECS': gadget_registry.Registry.get_all_specs(),
'INTERACTION_SPECS': interaction_registry.Registry.get_all_specs(),
'PANEL_SPECS': feconf.PANELS_PROPERTIES,
'DEFAULT_OBJECT_VALUES': rule_domain.get_default_object_values(),
'DEFAULT_OBJECT_VALUES': obj_services.get_default_object_values(),
'additional_angular_modules': additional_angular_modules,
'can_delete': rights_manager.Actor(
self.user_id).can_delete(
Expand Down Expand Up @@ -589,10 +589,10 @@ def get(self, exploration_id, escaped_state_name):

# The total number of possible answers is 100 because it requests the
# top 50 answers matched to the default rule and the top 50 answers
# matched to a fuzzy rule individually.
# matched to the classifier individually.
answers = stats_services.get_top_state_rule_answers(
exploration_id, state_name, [
exp_domain.DEFAULT_RULESPEC_STR, feconf.FUZZY_RULE_TYPE],
exp_domain.DEFAULT_RULESPEC_STR, feconf.CLASSIFIER_RULE_TYPE],
self.NUMBER_OF_TOP_ANSWERS_PER_RULE)

interaction = state.interaction
Expand All @@ -611,7 +611,7 @@ def get(self, exploration_id, escaped_state_name):
trained_answers = set()
for answer_group in interaction.answer_groups:
for rule_spec in answer_group.rule_specs:
if rule_spec.rule_type == feconf.FUZZY_RULE_TYPE:
if (rule_spec.rule_type == feconf.CLASSIFIER_RULE_TYPE):
trained_answers.update(
interaction_instance.normalize_answer(trained)
for trained
Expand Down
22 changes: 11 additions & 11 deletions core/controllers/editor_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ def _create_answer(value, count=1):
def _create_training_data(*arg):
return [_create_answer(value) for value in arg]

# Load the fuzzy rules demo exploration.
# Load the string classifier demo exploration.
exp_id = '15'
exp_services.load_demo(exp_id)
rights_manager.release_ownership_of_exploration(
Expand All @@ -264,7 +264,7 @@ def _create_training_data(*arg):
'%s/%s' % (feconf.EXPLORATION_INIT_URL_PREFIX, exp_id))
self.assertEqual(
exploration_dict['exploration']['title'],
'Demonstrating fuzzy rules')
'Demonstrating string classifier')

# This test uses the interaction which supports numeric input.
state_name = 'text'
Expand All @@ -279,7 +279,7 @@ def _create_training_data(*arg):
state_name].interaction.answer_groups
explicit_rule_spec_string = (
answer_groups[0].rule_specs[0].stringify_classified_rule())
fuzzy_rule_spec_string = (
classifier_spec_string = (
answer_groups[1].rule_specs[0].stringify_classified_rule())

# Input happy since there is an explicit rule checking for that.
Expand All @@ -291,14 +291,14 @@ def _create_training_data(*arg):
exp_id, 1, state_name, exp_domain.DEFAULT_RULESPEC_STR, 'sad')

# Input cheerful: this is current training data and falls under the
# fuzzy rule.
# classifier.
event_services.AnswerSubmissionEventHandler.record(
exp_id, 1, state_name, fuzzy_rule_spec_string, 'cheerful')
exp_id, 1, state_name, classifier_spec_string, 'cheerful')

# Input joyful: this is not training data but will be classified
# under the fuzzy rule.
# under the classifier.
event_services.AnswerSubmissionEventHandler.record(
exp_id, 1, state_name, fuzzy_rule_spec_string, 'joyful')
exp_id, 1, state_name, classifier_spec_string, 'joyful')

# Log in as an editor.
self.login(self.EDITOR_EMAIL)
Expand Down Expand Up @@ -337,13 +337,13 @@ def _create_training_data(*arg):
exploration_dict = self.get_json(
'%s/%s' % (feconf.EXPLORATION_INIT_URL_PREFIX, exp_id))

# If one of the values is added to the training data of a fuzzy
# rule, then it should not be returned as an unhandled answer.
# If one of the values is added to the training data of the
# classifier, then it should not be returned as an unhandled answer.
state = exploration_dict['exploration']['states'][state_name]
answer_group = state['interaction']['answer_groups'][1]
rule_spec = answer_group['rule_specs'][0]
self.assertEqual(
rule_spec['rule_type'], feconf.FUZZY_RULE_TYPE)
rule_spec['rule_type'], feconf.CLASSIFIER_RULE_TYPE)
rule_spec['inputs']['training_data'].append('joyful')

self.put_json('/createhandler/data/%s' % exp_id, {
Expand Down Expand Up @@ -390,7 +390,7 @@ def _create_training_data(*arg):
exploration_dict = self.get_json(
'%s/%s' % (feconf.EXPLORATION_INIT_URL_PREFIX, exp_id))

# If one of the existing training data elements in the fuzzy rule
# If one of the existing training data elements in the classifier
# is removed (5 in this case), but it is not backed up by an
# answer, it will not be returned as potential training data.
state = exploration_dict['exploration']['states'][state_name]
Expand Down
118 changes: 15 additions & 103 deletions core/controllers/reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,91 +97,6 @@ def test_can_play(self, exploration_id, **kwargs):
return test_can_play


def _evaluate_rule(rule_spec, rule_description, context_params, answer):
"""Evaluates a rule spec. Returns a float between 0.0 and 1.0."""
# NOTE TO DEVELOPERS: This function does not work. It must be removed
# prior to merge into develop.
pass


def classify_hard_rule(state, params, input_type, normalized_answer):
"""Find the first hard rule that matches."""
best_matched_answer_group = None
best_matched_answer_group_index = len(state.interaction.answer_groups)
best_matched_rule_spec_index = None
best_matched_truth_value = feconf.CERTAIN_FALSE_VALUE

interaction = interaction_registry.Registry.get_interaction_by_id(
state.interaction.id)

for (answer_group_index, answer_group) in enumerate(
state.interaction.answer_groups):
ored_truth_value = feconf.CERTAIN_FALSE_VALUE
for (rule_spec_index, rule_spec) in enumerate(
answer_group.rule_specs):
if rule_spec.rule_type != feconf.FUZZY_RULE_TYPE:
evaluated_truth_value = _evaluate_rule(
rule_spec,
interaction.get_rule_description(rule_spec.rule_type),
params, normalized_answer)
if evaluated_truth_value > ored_truth_value:
ored_truth_value = evaluated_truth_value
best_rule_spec_index = rule_spec_index
if ored_truth_value == feconf.CERTAIN_TRUE_VALUE:
best_matched_truth_value = ored_truth_value
best_matched_answer_group = answer_group
best_matched_answer_group_index = answer_group_index
best_matched_rule_spec_index = best_rule_spec_index
return {
'outcome': best_matched_answer_group.outcome.to_dict(),
'answer_group_index': best_matched_answer_group_index,
'classification_certainty': best_matched_truth_value,
'rule_spec_index': best_matched_rule_spec_index,
}

return None


def classify_soft_rule(state, params, input_type, normalized_answer):
"""Find the maximum soft rule that matches. This is done by ORing
(maximizing) all truth values of all rules over all answer groups. The
group with the highest truth value is considered the best match.
"""
best_matched_answer_group = None
best_matched_answer_group_index = len(state.interaction.answer_groups)
best_matched_rule_spec_index = None
best_matched_truth_value = feconf.CERTAIN_FALSE_VALUE

interaction = interaction_registry.Registry.get_interaction_by_id(
state.interaction.id)

for (answer_group_index, answer_group) in enumerate(
state.interaction.answer_groups):
fuzzy_rule_spec_index = answer_group.get_fuzzy_rule_index()
if fuzzy_rule_spec_index is not None:
fuzzy_rule_spec = answer_group.rule_specs[fuzzy_rule_spec_index]
else:
fuzzy_rule_spec = None
if fuzzy_rule_spec is not None:
evaluated_truth_value = _evaluate_rule(
fuzzy_rule_spec,
interaction.get_rule_description(fuzzy_rule_spec.rule_type),
params, normalized_answer)
if evaluated_truth_value == feconf.CERTAIN_TRUE_VALUE:
best_matched_truth_value = evaluated_truth_value
best_matched_rule_spec_index = fuzzy_rule_spec_index
best_matched_answer_group = answer_group
best_matched_answer_group_index = answer_group_index
return {
'outcome': best_matched_answer_group.outcome.to_dict(),
'answer_group_index': best_matched_answer_group_index,
'classification_certainty': best_matched_truth_value,
'rule_spec_index': best_matched_rule_spec_index,
}

return None


def classify_string_classifier_rule(state, normalized_answer):
"""Run the classifier if no prediction has been made yet. Currently this
is behind a development flag.
Expand All @@ -196,15 +111,15 @@ def classify_string_classifier_rule(state, normalized_answer):
[doc, []] for doc in state.interaction.confirmed_unclassified_answers]
for (answer_group_index, answer_group) in enumerate(
state.interaction.answer_groups):
fuzzy_rule_spec_index = answer_group.get_fuzzy_rule_index()
if fuzzy_rule_spec_index is not None:
fuzzy_rule_spec = answer_group.rule_specs[fuzzy_rule_spec_index]
classifier_spec_index = answer_group.get_classifier_index()
if classifier_spec_index is not None:
classifier_spec = answer_group.rule_specs[classifier_spec_index]
else:
fuzzy_rule_spec = None
if fuzzy_rule_spec is not None:
classifier_spec = None
if classifier_spec is not None:
training_examples.extend([
[doc, [str(answer_group_index)]]
for doc in fuzzy_rule_spec.inputs['training_data']])
for doc in classifier_spec.inputs['training_data']])
if len(training_examples) > 0:
sc.load_examples(training_examples)
doc_ids = sc.add_examples_for_predicting([normalized_answer])
Expand All @@ -216,8 +131,8 @@ def classify_string_classifier_rule(state, normalized_answer):
predicted_answer_group_index]
best_matched_truth_value = feconf.CERTAIN_TRUE_VALUE
for rule_spec in predicted_answer_group.rule_specs:
if rule_spec.rule_type == feconf.FUZZY_RULE_TYPE:
best_matched_rule_spec_index = fuzzy_rule_spec_index
if rule_spec.rule_type == feconf.CLASSIFIER_RULE_TYPE:
best_matched_rule_spec_index = classifier_spec_index
break
best_matched_answer_group = predicted_answer_group
best_matched_answer_group_index = predicted_answer_group_index
Expand All @@ -236,7 +151,7 @@ def classify_string_classifier_rule(state, normalized_answer):
# TODO(bhenning): Add more tests for classification, such as testing multiple
# rule specs over multiple answer groups and making sure the best match over all
# those rules is picked.
def classify(exp_id, state, answer, params):
def classify(state, answer):
"""Normalize the answer and select among the answer groups the group in
which the answer best belongs. The best group is decided by finding the
first rule best satisfied by the answer. Returns a dict with the following
Expand All @@ -260,16 +175,13 @@ def classify(exp_id, state, answer, params):
interaction_instance = interaction_registry.Registry.get_interaction_by_id(
state.interaction.id)
normalized_answer = interaction_instance.normalize_answer(answer)
response = None

input_type = interaction_instance.answer_type

response = classify_hard_rule(state, params, input_type, normalized_answer)
if response is None:
response = classify_soft_rule(
state, params, input_type, normalized_answer)
if (interaction_instance.is_string_classifier_trainable and
feconf.ENABLE_STRING_CLASSIFIER and response is None):
feconf.ENABLE_STRING_CLASSIFIER):
response = classify_string_classifier_rule(state, normalized_answer)
elif feconf.ENABLE_STRING_CLASSIFIER:
raise Exception('No classifier found for interaction.')

# The best matched group must match above a certain threshold. If no group
# meets this requirement, then the default 'group' automatically matches
Expand Down Expand Up @@ -497,7 +409,7 @@ class ClassifyHandler(base.BaseHandler):
REQUIRE_PAYLOAD_CSRF_CHECK = False

@require_playable
def post(self, exploration_id):
def post(self, unused_exploration_id):
"""Handles POST requests."""
# A domain object representing the old state.
old_state = exp_domain.State.from_dict(self.payload.get('old_state'))
Expand All @@ -507,7 +419,7 @@ def post(self, exploration_id):
params = self.payload.get('params')
params['answer'] = answer

self.render_json(classify(exploration_id, old_state, answer, params))
self.render_json(classify(old_state, answer))


class ReaderFeedbackHandler(base.BaseHandler):
Expand Down
Loading

0 comments on commit ee90b0c

Please sign in to comment.