Обучение с частичным привлечением учителя (Semi-Supervised Learning) - Лена Капаца
Обучение с частичным привлечением учителя (Semi-Supervised Learning) by Лена Капаца Feb. 25, 2021 Основы

Обучение с частичным привлечением учителя (полуавтоматическое обучение, частичное обучение) – алгоритм Машинного обучения (Machine Learning), который использует как размеченные, так и неразмеченные данные. Например, исследовав опухоли, установив их размер, плотность и другие метрики, мы передаем эти данные модели с обязательной пометкой, какое Наблюдение (Observation) к какому строению (доброкачественному или злокачественному) относится. Размеченные медданные изображены на верхней таблице, на нижней – неразмеченная клиентская база:

Полуавтоматическое обучение отличается от контролируемого, где используются только размеченные данные. Популярный подход здесь – это создание Графа (Graph), который группирует наблюдения в Тренировочных данных (Train Data) и присваивает соответствующие Ярлыки (Label) тем них, кто находится поблизости:

Алгоритм был предложен Сяоджином Чжу и Зубином Гахрамани в 2002 году в работе «Изучение данных с ярлыками и без с помощью распространения ярлыков». Сгенерированные графы группируют все наблюдения в Датасете (Dataset) на основе их расстояния:

Наблюдения с массивной белой обводкой с высокой вероятностью "принадлежат" кластерам

Распространение ярлыков (Label Propagation) подразумевает, что узлам графа присваиваются ярлыки, которые распространяются на неразмеченные элементы. Процесс повторяется фиксированное количество раз, чтобы поляризовать вероятность того или иного ярлыка, вплоть до Сходимости (Convergence), то есть сокращения ошибок.

Контролируемое обучение и Scikit-learn

Продемонстрируем частичное обучение в сравнении с контролируемым с помощью Scikit-learn. Для начала импортируем необходимые библиотеки:

from numpy import concatenate

import sklearn
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from sklearn.semi_supervised import LabelPropagation

Простая логистическая регрессия

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

# Сгенерируем данные
X, y = make_classification(n_samples = 1000, n_features = 2, n_informative = 2, n_redundant = 0, random_state = 1)

# Разделим датасет на тренировочную и тестовую части
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.50, random_state = 1, stratify = y)

# Разделим тренировочную часть на размеченную и неразмеченную подгруппы
X_train_lab, X_test_unlab, y_train_lab, y_test_unlab = train_test_split(X_train, y_train, test_size = 0.50, random_state = 1, stratify = y_train)

# Отобразим краткую сводку по тренировочной части датасета
print('Labeled Train Set:', X_train_lab.shape, y_train_lab.shape)
print('Unlabeled Train Set:', X_test_unlab.shape, y_test_unlab.shape)

# Отобразим краткую сводку по тестовой части датасета
print('Test Set:', X_test.shape, y_test.shape)

Вывод ячейки подтверждает, что у нас есть маркированные и немаркированные наборы тренировочных данных по 250 наблюдений каждый, а также тестовая часть длиной в 500 строк:

Размеченный тренировочный набор: (250, 2) (250,)
Неразмеченный тренировочный набор: (250, 2) (250,)
Тестовый набор: (500, 2) (500,)

У контролируемого обучения (Supervised Learning) было бы всего 250 наблюдений, у полуконтролируемого – 250 размеченных и еще столько же неразмеченных.

Используя алгоритм обучения с учителем, мы определим базовый уровень производительности для частичного обучения, и применим его к размеченной части данных. Это важно, потому что мы ожидаем, что алгоритм частичного обучения превзойдет алгоритм обучения с учителем. Если этого не случатся, тогда первый из них плохо настроен.

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

# Инициализируем модель логистической регрессии
model = LogisticRegression()

# Загрузим в модель размеченные данные
model.fit(X_train_lab, y_train_lab)

Модель продемонстрирует свои гиперпараметры, которые определяют стиль Штрафования (Penalty), веса классов Несбалансированного датасета (Imbalanced Dataset), количество задействованных ядер CPU (n_jobs) и т.д.:

LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
                   intercept_scaling=1, l1_ratio=None, max_iter=100,
                   multi_class='auto', n_jobs=None, penalty='l2',
                   random_state=None, solver='lbfgs', tol=0.0001, verbose=0,
                   warm_start=False)

Такую модель можно впоследствии использовать для прогнозирования неразделенной тестовой части датасета и сравнивать с использованием Точности измерений (Accuracy).

# Выполним тестовые предсказания
yhat = model.predict(X_test)

# Вычислим скор для тестового набора данных
score = accuracy_score(y_test, yhat)
print('Точность измерений: %.3f' % (score * 100))

Для такого синтетического датасета точность вполне удовлетворительная:

Точность измерений: 84.800

Результаты каждой попытки могут отличаться друг от друга: это происходит из-за стохастической (случайной) природы алгоритма и процедуры оценки. Для этого Дата-сайентисты (Data Scientist) запускают обучение несколько раз.

Теперь давайте посмотрим, как применить алгоритм распространения меток.

Логистическая регрессия и распространение меток

Алгоритм распространения меток прекрасно реализован в классе LabelPropagation. Инициализируем модель распространения меток. Важно отметить, что тренировочная часть данных, передаваемая функцией fit(), должна включать и размеченные, и неразмеченные примеры; последние с ярлыками "-1":

# Сгенерируем датасет
X, y = make_classification(n_samples = 1000, n_features = 2, n_informative = 2, n_redundant = 0, random_state = 1)

# Разделим датасет на тренировочную и тестовую части
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.50, random_state = 1, stratify = y)

# Разделим тренировочную часть на размеченную и неразмеченную подгруппы
X_train_lab, X_test_unlab, y_train_lab, y_test_unlab = train_test_split(X_train, y_train, test_size = 0.50, random_state = 1, stratify = y_train)

# create the training dataset input
X_train_mixed = concatenate((X_train_lab, X_test_unlab))

# Присвоим "-1" (отсутствие ярлыка) неразмеченной части датасета
nolabel = [-1 for _ in range(len(y_test_unlab))]

# Перекомбинируем ярлыки тренировочной части датасета 
y_train_mixed = concatenate((y_train_lab, nolabel))

Модель получает данные тем же образом – вызовом методов fit() и predict():

# Инициализируем модель распространения меток
model = LabelPropagation()

# Загрузим в модель тренировочные данные
model.fit(X_train_mixed, y_train_mixed)

# Выполним тестовые предсказания
yhat = model.predict(X_test)

Точность измерений приятно подросла:

Точность измерений: 85.600

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

Обучение с частичным привлечением учителя

Мы имеем доступ к оценкам с помощью класса transduction_ (англ, "передача"). Мы используем эти ярлыки вместе со всеми входными данными для обучения и оценки контролируемого алгоритма обучения. Еще раз сгенерируем данные:

# Сгенерируем датасет
X, y = make_classification(n_samples = 1000, n_features = 2, n_informative = 2, n_redundant = 0, random_state = 1)

# Разделим датасет на тренировочную и тестовую части
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.50, random_state = 1, stratify = y)

# Разделим тренировочную часть на размеченную и неразмеченную подгруппы
X_train_lab, X_test_unlab, y_train_lab, y_test_unlab = train_test_split(X_train, y_train, test_size = 0.50, random_state = 1, stratify = y_train)

# create the training dataset input
X_train_mixed = concatenate((X_train_lab, X_test_unlab))

# Присвоим "-1" (отсутствие ярлыка) неразмеченной части датасета
nolabel = [-1 for _ in range(len(y_test_unlab))]

# Перекомбинируем ярлыки тренировочной части датасета 
y_train_mixed = concatenate((y_train_lab, nolabel))

Алгоритм обучает "полууправляемую" модель на целостном датасете и пользуется оценками transduction_:

# Инициализируем модель распространения меток
model = LabelPropagation()

# Загрузим в модель тренировочные данные
model.fit(X_train_mixed, y_train_mixed)

# get labels for entire training dataset data
tran_labels = model.transduction_

# Инициализируем модель контролируемого обучения логистической регрессии
model2 = LogisticRegression()

# Загрузим в модель контролируемого обучения весь датасет
model2.fit(X_train_mixed, tran_labels)

# Выполним тестовые предсказания
yhat = model2.predict(X_test)

Вычислим Скор (Score) для тестового набора данных:

score = accuracy_score(y_test, yhat)
print('Точность измерений: %.3f' % (score * 100))

Такой иерархический подход обеспечивает наивысшую результативность предсказаний:

Точность измерений: 86.200

Ноутбук, не требующий дополнительной настройки на момент написания статьи, можно скачать здесь.

Фото: @sebastian_unrau

© Лена Капаца. Все права защищены.