| """ |
| Active Learning Module |
| |
| This module provides active learning capabilities for the annotation platform. |
| It implements machine learning algorithms to intelligently select which instances |
| should be annotated next, based on model confidence and disagreement scores. |
| |
| The active learning system: |
| 1. Trains classifiers on existing annotations |
| 2. Predicts confidence scores for unlabeled instances |
| 3. Reorders instances to prioritize those with low confidence |
| 4. Maintains a balance between active learning and random sampling |
| |
| This helps reduce the total number of annotations needed while maintaining |
| high-quality results by focusing on the most informative instances. |
| """ |
|
|
| def actively_learn(): |
| """ |
| Main active learning function that reorders instances based on model predictions. |
| |
| This function implements the core active learning algorithm: |
| 1. Collects all current annotations from users |
| 2. Resolves multiple annotations per instance using a specified strategy |
| 3. Trains classifiers for each annotation scheme |
| 4. Predicts confidence scores for unlabeled instances |
| 5. Reorders instances to prioritize low-confidence predictions |
| 6. Updates user assignment queues while preserving existing annotations |
| |
| Side Effects: |
| - Trains machine learning models on current annotations |
| - Reorders instance assignments for all users |
| - Updates active learning state tracking |
| - Logs training progress and statistics |
| |
| The function maintains a balance between active learning selection and |
| random sampling to ensure diversity in the training data. |
| """ |
| global user_to_annotation_state |
| global instance_id_to_data |
|
|
| |
| if "active_learning_config" not in config: |
| logger.warning( |
| "the server is trying to do active learning " + "but this hasn't been configured" |
| ) |
| return |
|
|
| al_config = config["active_learning_config"] |
|
|
| |
| if "enable_active_learning" in al_config and not al_config["enable_active_learning"]: |
| return |
|
|
| |
| if "classifier_name" not in al_config: |
| raise Exception('active learning enabled but no classifier is set with "classifier_name"') |
|
|
| if "vectorizer_name" not in al_config: |
| raise Exception('active learning enabled but no vectorizer is set with "vectorizer_name"') |
|
|
| if "resolution_strategy" not in al_config: |
| raise Exception("active learning enabled but resolution_strategy is not set") |
|
|
| |
| |
| |
| schema_used = [] |
| if "active_learning_schema" in al_config: |
| schema_used = al_config["active_learning_schema"] |
|
|
| |
| cls_kwargs = al_config.get("classifier_kwargs", {}) |
| cls_kwargs = al_config.get("classifier_kwargs", {}) |
| vectorizer_kwargs = al_config.get("vectorizer_kwargs", {}) |
| strategy = al_config["resolution_strategy"] |
|
|
| |
| |
| instance_to_labels = defaultdict(list) |
| for uas in user_to_annotation_state.values(): |
| for iid, annotation in uas.instance_id_to_labeling.items(): |
| instance_to_labels[iid].append(annotation) |
|
|
| |
| |
| |
| instance_to_label = {} |
| schema_seen = set() |
| for iid, annotations in instance_to_labels.items(): |
| resolved = resolve(annotations, strategy) |
|
|
| |
| if len(schema_used) > 0: |
| resolved = {k: resolved[k] for k in schema_used} |
|
|
| for s in resolved: |
| schema_seen.add(s) |
| instance_to_label[iid] = resolved |
|
|
| |
| texts = [] |
| |
| scheme_to_labels = defaultdict(list) |
| text_key = config["item_properties"]["text_key"] |
| for iid, schema_to_label in instance_to_label.items(): |
| |
| text = instance_id_to_data[iid][text_key] |
| texts.append(text) |
| for s in schema_seen: |
| |
| |
| label = schema_to_label.get(s, "DUMMY:NONE") |
|
|
| |
| |
| label = list(label.keys())[0] |
| scheme_to_labels[s].append(label) |
|
|
| scheme_to_classifier = {} |
|
|
| |
| for scheme, labels in scheme_to_labels.items(): |
|
|
| |
| |
| label_counts = Counter(labels) |
| if len(label_counts) < 2: |
| logger.warning( |
| ( |
| "In the current data, data labeled with %s has only a" |
| + "single unique label, which is insufficient for " |
| + "active learning; skipping..." |
| ) |
| % scheme |
| ) |
| continue |
|
|
| |
| cls = get_class(al_config["classifier_name"])(**cls_kwargs) |
| vectorizer = get_class(al_config["vectorizer_name"])(**vectorizer_kwargs) |
|
|
| |
| clf = Pipeline([("vectorizer", vectorizer), ("classifier", cls)]) |
| logger.info("training classifier for %s..." % scheme) |
| clf.fit(texts, labels) |
| logger.info("done training classifier for %s" % scheme) |
| scheme_to_classifier[scheme] = clf |
|
|
| |
| unlabeled_ids = [iid for iid in instance_id_to_data if iid not in instance_to_label] |
| random.shuffle(unlabeled_ids) |
|
|
| |
| |
| perc_random = al_config["random_sample_percent"] / 100 |
|
|
| |
| |
| random_ids = unlabeled_ids[int(len(unlabeled_ids) * perc_random) :] |
| unlabeled_ids = unlabeled_ids[: int(len(unlabeled_ids) * perc_random)] |
| remaining_ids = [] |
|
|
| |
| |
| if "max_inferred_predictions" in al_config: |
| max_insts = al_config["max_inferred_predictions"] |
| remaining_ids = unlabeled_ids[max_insts:] |
| unlabeled_ids = unlabeled_ids[:max_insts] |
|
|
| |
| |
| scheme_to_predictions = {} |
| unlabeled_texts = [instance_id_to_data[iid][text_key] for iid in unlabeled_ids] |
| for scheme, clf in scheme_to_classifier.items(): |
| logger.info("Inferring labels for %s" % scheme) |
| preds = clf.predict_proba(unlabeled_texts) |
| scheme_to_predictions[scheme] = preds |
|
|
| |
| |
| |
| ids_and_confidence = [] |
| logger.info("Scoring items by model confidence") |
| for i, iid in enumerate(tqdm(unlabeled_ids)): |
| most_confident_pred = 0 |
| mp_scheme = None |
| for scheme, all_preds in scheme_to_predictions.items(): |
|
|
| preds = all_preds[i, :] |
| mp = max(preds) |
| if mp > most_confident_pred: |
| most_confident_pred = mp |
| mp_scheme = scheme |
| ids_and_confidence.append((iid, most_confident_pred, mp_scheme)) |
|
|
| |
| |
| ids_and_confidence = sorted(ids_and_confidence, key=lambda x: x[1]) |
|
|
| |
| |
| new_id_order = [] |
| id_to_selection_type = {} |
| for (al, rand_id) in zip_longest(ids_and_confidence, random_ids, fillvalue=None): |
| if al: |
| new_id_order.append(al[0]) |
| id_to_selection_type[al[0]] = "%s Classifier" % al[2] |
| if rand_id: |
| new_id_order.append(rand_id) |
| id_to_selection_type[rand_id] = "Random" |
|
|
| |
| |
| new_id_order.extend(remaining_ids) |
|
|
| |
| |
| |
| |
| already_annotated = list(instance_to_labels.keys()) |
| for annotation_state in user_to_annotation_state.values(): |
| annotation_state.reorder_remaining_instances(new_id_order, already_annotated) |
|
|
| logger.info("Finished reordering instances") |