С реализацией Python

В этом посте мы представим тему самообучения, а затем реализуем две простые стратегии самообучения (почти) с нуля, чтобы закрепить наши знания.

Этот пост небесный на основе [1] и [2].

Но сначала давайте поговорим о том, что такое самообучение и почему это полезный инструмент, который стоит добавить в свой набор инструментов специалиста по обработке и анализу данных.

К настоящему времени мы все знаем, что количество доступных данных значительно выросло, что привело к возрождению моделей глубокого обучения, что позволило области машинного обучения в целом достичь нового уровня производительности.

Однако, даже при наличии большого количества данных, помеченных данных мало. Это связано с тем, что маркировать данные сложно, дорого и медленно. Следовательно, у нас есть огромные объемы неразмеченных данных, которые мы должны осмыслить.

С другой стороны, наиболее зрелой областью машинного обучения является контролируемое обучение. Как многие из нас знают, справиться с неконтролируемым обучением может быть довольно сложно. Из-за этого, что лучше, иметь немного помеченных данных или использовать полностью неконтролируемый подход?

Что ж, я не в состоянии ответить на этот вопрос, но из-за этого появилась область полуконтролируемого обучения. В этой области основное внимание уделяется осмыслению огромного количества неразмеченных данных с использованием нескольких точек размеченных данных. Этот сценарий обычно встречается в задачах с графами.

Самообучение — это один из подходов к полуконтролируемому обучению. Давайте углубимся в это.

Код для этого поста доступен на моем GitHub и на Kaggle.

Говоря о самообучении

Основная идея самообучения заключается в использовании как размеченных, так и неразмеченных данных, позволяющих вашей модели (или моделям) учиться у самих себя (или друг у друга).

Это процесс, в котором вы пытаетесь не только научить модель шаблонам данных, но и тому, как она может использовать немаркированные данные для продолжения обучения.

В этом посте будут рассмотрены два метода самообучения: псевдомаркировка и самообучение с двумя классификаторами.

Псевдомаркировка

Идея псевдомаркировки состоит в том, чтобы генерировать метки для немаркированных данных, а затем использовать эти новые помеченные данные для переобучения модели с использованием большего количества информации, чем она видела ранее.

Конечно, это может быть проблематично, когда ваша модель ошибается, поэтому мы не предсказываем весь набор тестов, а затем переобучаем модель. Мы будем повторять, добавляя только те прогнозы, которые наша модель более надежна. И мы делаем это, выбирая высшие вероятности класса, поэтому, чтобы это работало, мы должны использовать калиброванные классификаторы [2].

Давайте начнем наш код с определения того, сколько итераций псевдомаркировки мы собираемся использовать. Мы также загрузим набор данных Wine и определим некоторые параметры для проверки, чтобы увидеть, как метод улучшает или ухудшает результат:

pseudo_label_iters = 20
thresholds = [0.4, 0.6, 0.8, 1]
null_percs = [0.2, 0.5, 0.7]
# Data Loading
df = datasets.load_wine()

Мы сравним результаты с классификатором, который не был обучен на псевдоразмеченных данных. Мы будем перебирать процент нулей и порог, чтобы определить, какой прогноз является высоконадежным.

Мы также откалибруем наш классификатор и оптимизируем его с помощью оптимизатора гиперпараметров GridSearch.

rf = RandomForestClassifier(n_jobs=-1)
parameters = {
    'n_estimators': [10, 50],
    'class_weight': [None, 'balanced'],
    'max_depth': [None, 5, 10]
}
results = pd.DataFrame()
for threshold in tqdm(thresholds):
    for null_perc in null_percs:
        
        # Creating a test set for us to validate our results (and compare to a non-self-learning classifier)
        X_train, X_test, y_train, y_test = train_test_split(
            df.data, df.target, test_size=0.3, shuffle=True)
        
        # Randomly removing null_perc % of labels from training set
        rng = np.random.RandomState()
        random_unlabeled_points = rng.rand(y_train.shape[0]) < null_perc
y_train[random_unlabeled_points] = -1
        new_y_train = y_train.copy()
# Training loop
        for i in range(pseudo_label_iters):
# Select the labeled set
            X = X_train[np.where(new_y_train != -1)]
            y = new_y_train[np.where(new_y_train != -1)]
# Select the unlabeled set
            X_un = X_train[np.where(new_y_train == -1)]
            y_un = new_y_train[np.where(new_y_train == -1)]
if len(y_un) == 0:
                break
# Hyperparameter optimization
            rf_ = GridSearchCV(rf, parameters, cv=2).fit(X, y).best_estimator_
# Probability Calibration    
            calibrated_clf = CalibratedClassifierCV(base_estimator=rf_,
                                                    cv=2,
                                                    ensemble=False)
            calibrated_clf.fit(X, y)
            preds = calibrated_clf.predict_proba(X_un)
# Adding the high confidence labels
            classes = np.argmax(preds, axis=1)
            classes_probabilities = np.max(preds, axis=1)
high_confidence_classes = classes[np.where(classes_probabilities >= threshold)]
y_un[np.where(classes_probabilities >= threshold)] = high_confidence_classes
new_y_train[np.where(new_y_train == -1)] = y_un
# Validation
        X = X_train[np.where(new_y_train != -1)]
        y = new_y_train[np.where(new_y_train != -1)]
        calibrated_clf.fit(X, y)
y_pred_self_learning = calibrated_clf.predict(X_test)
X = X_train[np.where(y_train != -1)]
        y = y_train[np.where(y_train != -1)]
calibrated_clf.fit(X, y)
        y_pred = calibrated_clf.predict(X_test)
        
        results = pd.concat([results, pd.DataFrame([{
            'threshold': threshold, 'null_perc': null_perc,
            'normal_acc': accuracy_score(y_test, y_pred),
            'pseudo_acc': accuracy_score(y_test, y_pred_self_learning)
        }])])

Здесь мы видим результаты, когда мы меняем процент нулей в данных:

И здесь у нас есть пороговое изменение:

Как мы видим, метод самообучения начинает давать в среднем лучшие результаты, а чем меньше вариаций, тем больше нулей и тем увереннее мы в прогнозах. Тем не менее, разница не столь существенна, в основном потому, что мы имеем дело с «простым» набором данных, который довольно мал.

Два самообучающихся классификатора

Этот тип самообучения выглядит как псевдомаркировка, но вместо того, чтобы каждый раз использовать один и тот же классификатор, мы будем использовать два классификатора. На каждом этапе мы будем обучать классификатор на доступных данных, а затем будем прогнозировать следующую порцию новых данных. Затем мы переключаем классификатор, который используем, и делаем это несколько раз.

При этом мы пытаемся сделать модель более надежной, поскольку мы всегда подаем новый классификатор на вход предыдущего.

Для этой реализации мы будем полагаться на концепции, которые мы создали для псевдомаркировки.

from sklearn.neural_network import MLPClassifier
rf = RandomForestClassifier(n_jobs=-1)
mlp = MLPClassifier()
rf_param = {
    'n_estimators': [10, 50],
    'class_weight': [None, 'balanced'],
    'max_depth': [None, 5, 10]
}
mlp_param = {
    'hidden_layer_sizes': [(50,), (50, 50), (5, 50, 50)],
    'alpha': [0.0001, 0.001, 0.01]
}
results = pd.DataFrame()
for threshold in tqdm(thresholds):
    for null_perc in null_percs:
        
        # Creating a test set for us to validate our results (and compare to a non-self-learning classifier)
        X_train, X_test, y_train, y_test = train_test_split(
            df.data, df.target, test_size=0.3, shuffle=True)
        
        # Normalizing the data
        scaler = StandardScaler()
        X_train = scaler.fit_transform(X_train)
        X_test = scaler.transform(X_test)
        
        # Randomly removing null_perc % of labels from training set
        rng = np.random.RandomState()
        random_unlabeled_points = rng.rand(y_train.shape[0]) < null_perc
y_train[random_unlabeled_points] = -1
        new_y_train = y_train.copy()
# Training loop
        for i in range(pseudo_label_iters):
            # Choose the classifier to use
            if i % 2 == 0:
                clf = rf
                parameters = rf_param
            else:
                clf = mlp
                parameters = mlp_param
# Select the labeled set
            X = X_train[np.where(new_y_train != -1)]
            y = new_y_train[np.where(new_y_train != -1)]
# Select the unlabeled set
            X_un = X_train[np.where(new_y_train == -1)]
            y_un = new_y_train[np.where(new_y_train == -1)]
if len(y_un) == 0:
                break
# Hyperparameter optimization
            clf_ = GridSearchCV(clf, parameters, cv=2).fit(X, y).best_estimator_
# Probability Calibration    
            calibrated_clf = CalibratedClassifierCV(base_estimator=clf_,
                                                    cv=2,
                                                    ensemble=False)
            calibrated_clf.fit(X, y)
            preds = calibrated_clf.predict_proba(X_un)
# Adding the high confidence labels
            classes = np.argmax(preds, axis=1)
            classes_probabilities = np.max(preds, axis=1)
high_confidence_classes = classes[np.where(classes_probabilities >= threshold)]
y_un[np.where(classes_probabilities >= threshold)] = high_confidence_classes
new_y_train[np.where(new_y_train == -1)] = y_un
# Validation
        X = X_train[np.where(new_y_train != -1)]
        y = new_y_train[np.where(new_y_train != -1)]
        calibrated_clf.fit(X, y)
y_pred_self_learning = calibrated_clf.predict(X_test)
X = X_train[np.where(y_train != -1)]
        y = y_train[np.where(y_train != -1)]
calibrated_clf.fit(X, y)
        y_pred = calibrated_clf.predict(X_test)
        
        results = pd.concat([results, pd.DataFrame([{
            'threshold': threshold, 'null_perc': null_perc,
            'normal_acc': accuracy_score(y_test, y_pred),
            'pseudo_acc': accuracy_score(y_test, y_pred_self_learning)
        }])])

Что касается результатов, у нас есть нулевая процентная вариация:

И пороговое изменение:

Поскольку в этом случае мы используем два классификатора, справедливо ожидать, что больший объем данных будет лучше, даже если он не будет помечен. Как видно из результатов, метод самообучения едва смог обойти классификатор по умолчанию.

Заключение

Как я всегда говорю, наука о данных — это решение бизнес-задач с использованием правильных инструментов, а самообучение — это еще один инструмент, который вы должны иметь в своем наборе инструментов, чтобы помочь вам справиться с новым набором проблем.

Об этом можно еще много говорить, но я надеюсь, что это первоначальное введение поможет вам начать работу.

[1] Амини, Массих-Реза и Феофанов, Василий и Паулетто, Лоик и Девивер, Эмили и Максимов, Юрий. (2022). Самообучение: обзор.

[2] https://scikit-learn.org/stable/modules/calibration.html#калибровка