Мысли и теория

Вероятностное машинное обучение и слабое наблюдение

Внедрение экспертных знаний в предметной области и вероятностной маркировки в машинное обучение с помощью scikit-learn: альтернативы ручной маркировке.

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

Растет область слабого надзора, в которой МСП определяют эвристики, которые затем использует система для вывода о немаркированных данных, система вычисляет некоторые потенциальные метки, а затем МСП оценивает метки, чтобы определить, где может потребоваться дополнительная эвристика. быть добавленным или измененным. Например, при построении модели того, была ли операция необходима на основе медицинских записей, SME может предоставить следующую эвристику: если транскрипция содержит термин «анестезия» (или похожее на него регулярное выражение), то операция, скорее всего, произошла.

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

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

Мы представляем три ключевых показателя, которые специалист по обработке данных или эксперт в предметной области (SME) может использовать для оценки производительности вероятностных моделей и использовать производительность, закодированную цифрами, для повторения модели:

  • Распределение базовой ставки,
  • Матрица вероятностной ошибки и
  • Распределение предсказаний меток.

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

  • Распределение базовой ставки кодирует нашу неопределенность в отношении баланса классов и будет полезным инструментом, поскольку мы хотим, чтобы наши предсказанные метки уважали базовую ставку в данных;
  • Когда у нас есть категориальные метки, мы используем матрицу путаницы для оценки производительности модели; теперь, когда у нас есть вероятностные метки, мы вводим обобщенную матрицу вероятностной ошибки для оценки производительности модели;
  • График распределения предсказаний меток показывает нам полное распределение наших вероятностных предсказаний; как мы увидим, очень важно, чтобы это распределение учитывало то, что мы знаем о нашей базовой ставке (например, если наша базовая ставка для «Хирургия» в приведенном выше примере составляет 25%, в распределении меток мы ожидаем увидеть ~ 25 % нашего набора данных с высокой вероятностью хирургического вмешательства и ~ 75% из этого набора данных с низким шансом на операцию.)

А теперь за работу! Шаги, которые мы пройдем, следующие:

  • Вручную пометить небольшой объем данных и установить базовую ставку;
  • Создайте некоторые эвристики / хинтеры, ориентированные на предметную область;
  • Оцените производительность нашей модели, взглянув на матрицу вероятностной ошибки и график распределения меток;
  • Создавайте больше хинтеров, чтобы улучшить качество наших этикеток.

Вы можете найти весь соответствующий код в этом репозитории Github.

Итак, приступим к маркировке!

Ручные этикетки и базовые ставки

Первый шаг в рабочем процессе - вручную пометить небольшой образец данных. Данные, с которыми мы будем работать здесь, - это набор данных медицинских транскрипций от Kaggle (Лицензия CC0), и задача состоит в том, чтобы предсказать, связана ли какая-либо данная транскрипция с хирургическим вмешательством, как указано в столбце медицинская специальность. Мы будем использовать этот столбец, чтобы вручную пометить несколько строк в педагогических целях, но обычно это SME, просматривающий транскрипцию, чтобы пометить строки. Давайте посмотрим на наши данные:

# import packages and data
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import warnings; warnings.simplefilter(‘ignore’)
sns.set()
df = pd.read_csv(‘data/mtsamples.csv’)
df.head()

Проверив наши данные, давайте теперь вручную пометим некоторые строки, используя столбец «medical_specialty»:

# hand label some rows
N = 250
df = df.sample(frac=1, random_state=42).reset_index()
df_labeled = df.iloc[:N]
df_unlabeled = df.iloc[N:]
df_labeled[‘label’] = (df_labeled[‘medical_specialty’].str.contains(‘Surgery’) == 1).astype(int)
df_unlabeled[‘label’] = None
df_labeled.head()

Эти действия ручной маркировки служат двум целям:

  • чтобы научить нас балансу классов и базовой ставке
  • для создания набора для проверки

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

base_rate = sum(df_labeled[‘label’])/len(df_labeled)
f”Base rate = {base_rate}”

Выход:

‘Base rate = 0.24'

Распределение базовой ставки

Пришло время ввести первый показатель: распределение базовой ставки. Что мы подразумеваем здесь под распределением, учитывая, что у нас есть фактическая базовая ставка? Что ж, один из способов подумать об этом состоит в том, что мы рассчитали базовую ставку на основе выборки данных. Это означает, что мы не знаем точной базовой ставки, и нашу неопределенность в отношении нее можно охарактеризовать распределением. Одним из технических способов сформулировать эту неопределенность является использование байесовских методов, и, по сути, наши знания о базовой скорости кодируются с помощью апостериорного распределения. Вам не нужно слишком много знать о байесовских методах, чтобы понять суть этого, но если вы хотите узнать больше, вы можете ознакомиться с вводным материалом здесь. В блокноте мы написали функцию, которая строит распределение базовой ставки, а затем строим график распределения для данных, которые мы вручную пометили выше (орел заметит, что мы масштабировали наше распределение вероятностей так, чтобы он имеет пик при y = 1; мы сделали это в педагогических целях, и все относительные вероятности остались прежними).

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

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

  • по мере того, как вы генерируете все больше и больше данных, ваши апостериорные данные становятся уже, то есть вы становитесь все более и более уверенными в своей оценке.
  • вам нужно больше данных, чтобы быть уверенным в своей оценке при p = 0,5, а не при p = 0 или p = 1.

Ниже мы построили распределение базовой ставки для p = 0,5 и увеличения N (N = 5, 20, 50, 100). В записной книжке вы можете построить интерактивную фигуру с помощью виджета!

Машинная маркировка с указателями домена

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

Врач, например, может знать, что если в транскрипции есть термин «АНЕСТЕЗИЯ», то весьма вероятно, что операция произошла. Этот тип знания, однажды закодированный для вычислений, известен как хинтер.

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

  • увеличение P (хирургия) от базовой скорости, если транскрипция включает термин «АНЕСТЕЗИЯ» и
  • ничего не делать, если этого не происходит (мы предполагаем, что отсутствие термина здесь не сигнализирует).

Есть много способов увеличить P (хирургия), и для простоты мы берем среднее значение текущего P (хирургия) и веса W (вес обычно указывается SME и кодирует, насколько они уверены в том, что хинтер коррелирован. с положительными результатами).

# Combine labeled and unlabeled to retrieve our entire dataset
df1 = pd.concat([df_labeled, df_unlabeled])
# Check out how many rows contain the term of interest
df1[‘transcription’].str.contains(‘ANESTHESIA’).sum()
# Create column to encode hinter result
df1[‘h1’] = df1[‘transcription’].str.contains(‘ANESTHESIA’)
## Hinter will alter P(S): 1st approx. if row is +ve wrt hinter, take average; if row is -ve, do nothing
## OR: if row is +ve, take average of P(S) and weight; if row is -ve
##
## Update P(S) as follows
## If h1 is false, do nothing
## If h1 is true, take average of P(S) and weight (95), unless labeled
W = 0.95
L = []
for index, row in df1.iterrows():
 if df1.iloc[index][‘h1’]:
 P1 = (base_rate + W)/2
 L.append(P1)
 else:
 P1 = base_rate
 L.append(P1)
df1[‘P1’] = L
# Check out what our probabilistic labels look like
df1.P1.value_counts()

Выход:

0.240 3647
0.595 1352
Name: P1, dtype: int64

Теперь, когда мы обновили нашу модель с помощью хинтера, давайте подробнее рассмотрим, как работает наша модель. Две самые важные вещи, которые необходимо проверить:

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

Для первого вопроса введите вероятностную матрицу неточностей.

Матрица вероятностной ошибки

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

В вероятностной матрице путаницы ваша ось Y - это ваши ручные метки, а ось X - это прогноз модели. Но в этом случае предсказание модели - это вероятность, в отличие от «да» или «нет» в классической матрице путаницы.

plt.subplot(1, 2, 1)
df1[df1.label == 1].P1.hist();
plt.xlabel(“Probabilistic label”)
plt.ylabel(“Count”)
plt.title(“Hand Labeled ‘Surgery’”)
plt.subplot(1, 2, 2)
df1[df1.label == 0].P1.hist();
plt.xlabel(“Probabilistic label”);
plt.title(“Hand Labeled ‘Not Surgery’”);

Мы видим здесь, что

  • Большинство данных с пометкой «Хирургия» имеют P (S) около 0,60 (хотя и не намного), а у остальных около 0,24;
  • Во всех строках с пометкой «Not Surgery» P (S) составляет около 0,24, а в остальных - около 0,60.

Это хорошее начало, поскольку P (S) смещен влево для тех, кто помечен как «Не хирургия», и вправо для тех, кто помечен как «Хирургия». Тем не менее, мы бы хотели, чтобы он был смещен намного ближе к P (S) = 1 для тех, кто помечен как «Хирургия», так что еще есть над чем поработать.

График распределения этикеток

Следующий ключевой график - это график распределения предсказаний меток по набору данных: мы хотим увидеть, как распределяются наши предсказания меток и соответствует ли это тому, что мы знаем о нашей базовой ставке. Например, в нашем случае мы знаем, что наша базовая ставка, вероятно, составляет около 25%. Таким образом, мы ожидаем увидеть в распределении меток ~ 25% нашего набора данных с почти 100% вероятностью хирургического вмешательства и ~ 75% из них с низкой вероятностью хирургического вмешательства.

df1.P1.plot.kde();

Мы видим пики в ~ 25% и ~ 60%, что означает, что наша модель еще не имеет четкого представления о ярлыке, и поэтому мы хотим добавить больше подсказок.

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

Создание большего количества хинтеров

Чтобы создавать более качественные ярлыки, мы можем привнести больше знаний в предметную область, добавив больше хинтеров. Мы делаем это ниже, добавляя еще два положительных хинтера (те, которые коррелируют с «операцией»), которые изменяют нашу прогнозируемую вероятность аналогично хинтеру выше. Смотрите код в записной книжке. Вот матрица вероятностной ошибки и график распределения меток:

По мере увеличения количества хинтеров произошло две вещи:

  1. В нашей вероятностной матрице неточностей мы видели, как гистограмма для руки с надписью «Хирургия» перемещается вправо, и это хорошо! Мы также видели, как гистограмма для руки с надписью «Хирургия» немного сдвинулась вправо, что нам не нужно. Обратите внимание, что это связано с тем, что мы ввели только положительные хинтеры, поэтому мы можем захотеть ввести затем отрицательные хинтеры или использовать более сложный метод перехода от хинтеров к вероятностным меткам.
  2. Наш график распределения меток теперь имеет большую плотность выше P (S) = 0,5 (больше плотности справа), что также желательно. Напомним, что мы ожидали увидеть в распределении меток ~ 25% нашего набора данных с почти 100% вероятностью хирургического вмешательства и ~ 75% этого показателя с низкой вероятностью хирургического вмешательства.

Генеративные модели для хинтеров

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

Мы также будем использовать более изощренный метод перехода от хинтера к вероятностной метке: вместо усреднения по весу и предыдущему вероятностному прогнозу мы будем использовать наивную байесовскую модель, которая является генеративной моделью. Генеративная модель - это модель, которая моделирует совместную вероятность P (X, Y) признаков X и целевой Y, в отличие от дискриминативных моделей, которые моделируют условную вероятность P (Y | X) целевой условной по особенностям. Преимущество использования генеративной модели в отличие от дискриминантной модели, такой как случайный лес, заключается в том, что она позволяет нам моделировать сложные отношения между данными, целевой переменной и указателями: она позволяет нам отвечать на такие вопросы, как Какие хинтеры шумнее других? и "В каких случаях они шумные?" Подробнее о генеративных моделях у Google есть хорошее введение здесь.

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

# List of positive hinters
pos_hinters = [‘anesthesia’, ‘subcuticular’, ‘sponge’, ‘retracted’, ‘monocryl’, ‘epinephrine’, ‘suite’, ‘secured’, ‘nylon’, ‘blunt dissection’, ‘irrigation’, ‘cautery’, ‘extubated’,‘awakened’, ‘lithotomy’, ‘incised’, ‘silk’, ‘xylocaine’, ‘layers’, ‘grasped’, ‘gauge’, ‘fluoroscopy’, ‘suctioned’, ‘betadine’, ‘infiltrated’, ‘balloon’, ‘clamped’]
# List of negative hinters
neg_hinters = [‘reflexes’, ‘pupils’, ‘married’, ‘cyanosis’, ‘clubbing’, ‘normocephalic’, ‘diarrhea’, ‘chills’, ‘subjective’]

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

for hinter in pos_hinters:
 df1[hinter] = df1[‘transcription’].str.contains(hinter, na=0).astype(int)
 # print(df1[hinter].sum())
for hinter in neg_hinters:
 df1[hinter] = -df1[‘transcription’].str.contains(hinter, na=0).astype(int)
 # print(df1[hinter].sum())

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

# extract labeled data
df_lab = df1[~df1[‘label’].isnull()]
#df_lab.info()
# convert to numpy arrays
X = df_lab[pos_hinters].to_numpy()
y = df_lab[‘label’].to_numpy().astype(int)
## split into training and validation sets
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
 X, y, test_size=0.33, random_state=42)

Теперь мы обучаем наивный байесовский алгоритм Бернулли (или бинарный) на обучающих данных:

# Time to Naive Bayes!
from sklearn.naive_bayes import BernoulliNB
clf = BernoulliNB(class_prior=[base_rate, 1-base_rate])
clf.fit(X_train, y_train);

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

probs_test = clf.predict_proba(X_test)
df_val = pd.DataFrame({‘label’: y_test, ‘pred’: probs_test[:,1]})
plt.subplot(1, 2, 1)
df_val[df_val.label == 1].pred.hist();
plt.xlabel(“Probabilistic label”)
plt.ylabel(“Count”)
plt.title(“Hand Labeled ‘Surgery’”)
plt.subplot(1, 2, 2)
df_val[df_val.label == 0].pred.hist();
plt.xlabel(“Probabilistic label”)
plt.title(“Hand Labeled ‘Not Surgery’”);

Это круто! Используя больше подсказок и наивную байесовскую модель, мы видим, что нам удалось увеличить количество истинно положительных и истинно отрицательных. Это видно на графиках выше, поскольку гистограмма для метки на руке «Хирургия» смещена больше вправо, а гистограмма для «Не хирургия» - влево.

Теперь давайте изобразим весь график распределения меток (чтобы быть технически правильным, нам нужно было бы усечь этот KDE при x = 0 и x = 1, но для педагогических целей у нас все в порядке, так как это мало что изменит):

probs_all = clf.predict_proba(df1[pos_hinters].to_numpy())
df1[‘pred’] = probs_all[:,1]
df1.pred.plot.kde();

Если вы спрашиваете: «Когда пора прекратить навешивать ярлыки?», Вы задаете ключевой и принципиально сложный вопрос. Давайте разделим это на 2 вопроса: ‍

  • Когда прекратить ручную маркировку?
  • Когда остановить весь процесс? Т.е. Когда вы перестаете создавать хинтеров?

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

Теперь, когда вы перестанете добавлять хинтеров? Это аналогично вопросу: «Когда вы будете достаточно уверены в себе, чтобы начать обучение модели на этих данных?» и ответ будет варьироваться от ученого к ученому. Вы могли бы сделать это качественно, прокручивая и глядя, но большинство ученых предпочли бы количественный метод. Самый простой способ - рассчитать такие метрики, как точность, точность, отзывчивость и оценка F1 ваших данных с вероятностной меткой по сравнению с данными, размеченными вручную, а самый простой способ сделать это - использовать порог для ваших вероятностных меток: например , метка ‹10% будет равна 0,› 90% 1 или что-то среднее между воздержанием. Чтобы быть ясным, такие вопросы все еще являются активной областью исследования в Watchful… Следите за этим пространством!

Авторы Хьюго Боун-Андерсон и Шаян Моханти

Большое спасибо Эрику Ма за его отзыв о рабочем проекте этого сообщения.

Первоначально опубликовано на https://www.watchful.io.