Машинное обучение с помощью Python в наборе данных Enron

Расследование мошенничества с помощью Scikit-learn

Примечание автора: Следующий проект машинного обучения был завершен в рамках Udacity Data Analyst Nanodegree, который я завершил в мае 2017 года. Весь код можно найти в моем репозитории GitHub для этого класса. . Я настоятельно рекомендую этот курс всем, кто интересуется анализом данных (то есть всем, кто хочет разобраться в огромных объемах данных, генерируемых в нашем современном мире), а также тем, кто хочет получить базовые навыки программирования в формате, ориентированном на проекты. .

Вступление

Фон набора данных

Электронная почта + набор финансовых данных Enron - это кладезь информации о Enron Corporation, энергетической, сырьевой и сервисной компании, которая обанкротилась в декабре 2001 года в результате мошенничества. После краха компании Федеральная комиссия по регулированию энергетики выпустила более 1,6 миллиона электронных писем, отправленных и полученных руководителями Enron в период с 2000 по 2002 год (История Enron). После многочисленных жалоб на конфиденциальный характер электронных писем FERC отредактировала большую часть электронных писем, но около 0,5 миллиона остаются общедоступными. Электронная почта + финансовые данные содержат сами электронные письма, метаданные об электронных письмах, такие как количество, полученное и отправленное каждым человеком, а также финансовую информацию, включая зарплату и опционы на акции. Набор данных Enron стал ценным полигоном для обучения и тестирования практиков машинного обучения, чтобы попытаться разработать модели, которые могут идентифицировать лиц, представляющих интересы (POI), по функциям в данных. Заинтересованные лица - это лица, которых в конечном итоге судили за мошенничество или преступную деятельность в рамках расследования Enron, и в их число входят несколько руководителей высшего звена. Целью этого проекта было создание модели машинного обучения, которая могла бы разделять POI. Я предпочитаю использовать не текст, содержащийся в электронных письмах, в качестве входных данных для моего классификатора, а скорее метаданные об электронных письмах и финансовой информации. Конечная цель исследования набора данных Enron состоит в том, чтобы заблаговременно предсказать случаи мошенничества или небезопасной деловой практики, чтобы виновные могли быть наказаны, а невиновные не пострадали. Машинное обучение обещает мир, в котором больше не будет Enron, так что приступим!

Электронная почта и набор финансовых данных Enron, а также несколько предварительных функций, используемых в этом отчете, доступны на сайте Udacity’s Machine Learning Engineer GitHub.

Исследование выбросов и очистка данных

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

<class 'pandas.core.frame.DataFrame'>
Index: 146 entries, ALLEN PHILLIP K to YEAP SOON
Data columns (total 20 columns):
poi                          146 non-null bool
salary                       95 non-null float64
bonus                        82 non-null float64
long_term_incentive          66 non-null float64
deferred_income              49 non-null float64
deferral_payments            39 non-null float64
loan_advances                4 non-null float64
other                        93 non-null float64
expenses                     95 non-null float64
director_fees                17 non-null float64
total_payments               125 non-null float64
exercised_stock_options      102 non-null float64
restricted_stock             110 non-null float64
restricted_stock_deferred    18 non-null float64
total_stock_value            126 non-null float64
to_messages                  86 non-null float64
from_messages                86 non-null float64
from_poi_to_this_person      86 non-null float64
from_this_person_to_poi      86 non-null float64
shared_receipt_with_poi      86 non-null float64
dtypes: bool(1), float64(19)
memory usage: 23.0+ KB

Из информации о наборе данных я вижу, что все поля представляют собой числа с плавающей запятой, за исключением идентификатора poi, который имеет значение True / False. Во фрейме данных 146 строк, что, скорее всего, означает, что есть информация о 146 людях. Некоторые из максимальных значений кажутся необоснованными, например, общие выплаты, составляющие 309 миллионов долларов, или 15 000 сообщений на сообщения! Это могут быть допустимые значения, но они выглядят подозрительно при первом просмотре информации.

Другое наблюдение: существует множество NaN как в электронной почте, так и в финансовой сфере. Согласно официальной документации в формате pdf для финансовых (платежных и складских) данных, значения NaN представляют собой 0, а не неизвестные величины. Однако для данных электронной почты NaN - это неизвестная информация. Поэтому я заменю любые финансовые данные, которые являются NaN, на 0, но заполню NaN для данных электронной почты средним значением столбца, сгруппированным по интересующим лицам. Другими словами, если у человека есть значение NaN для to_messages, и он представляет интерес, я заполню это значение средним значением to_messages для интересующего человека. Если бы я решил отбросить все NaN, это уменьшило бы размер того, что уже является небольшим набором данных. Поскольку качество модели машинного обучения пропорционально количеству вводимых в нее данных, я не решаюсь удалять любую информацию, которая могла бы быть полезной.

from sklearn.preprocessing import Imputer

# Fill in the NaN payment and stock values with zero 
df[payment_data] = df[payment_data].fillna(0)
df[stock_data] = df[stock_data].fillna(0)

# Fill in the NaN email data with the mean of column grouped by poi/ non_poi
imp = Imputer(missing_values='NaN', strategy = 'mean', axis=0)

df_poi = df[df['poi'] == True];
df_nonpoi = df[df['poi']==False]

df_poi.ix[:, email_data] = imp.fit_transform(df_poi.ix[:,email_data]);
df_nonpoi.ix[:, email_data] = imp.fit_transform(df_nonpoi.ix[:,email_data]);
df = df_poi.append(df_nonpoi)

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

errors = (df[df[payment_data[:-1]].sum(axis='columns') != df['total_payments']])
errors

Разумеется, есть два человека, сумма платежей которых не соответствует зарегистрированной общей сумме платежей. Ошибки, по-видимому, вызваны несовпадением столбцов; для Роберта Белфера финансовые данные были сдвинуты на один столбец вправо, а для Санджая Бхатнагара данные были сдвинуты на один столбец влево. Я могу переместить столбцы на их правильные позиции, а затем снова проверить, совпадают ли значения отдельных платежей и запасов с соответствующими итоговыми значениями.

# Check for any more errors with the payment data len(df[df[payment_data[:-1]].sum(axis='columns') != df['total_payments']])
Output: 
0
# Check for any errors with the stock data
len(df[df[stock_data[:-1]].sum(axis='columns') != df['total_stock_value']])
Output:
0

Исправление смещенных финансовых данных устранило две ошибки, которых больше не видно при изучении набора данных. Однако, просматривая официальный финансовый PDF-файл, я вижу, что мне нужно удалить «ИТОГО», так как в настоящее время это последняя строка фрейма данных, и это нарушит любые прогнозы. (Итоговая строка - это то, что отображало подозрительные максимальные числа в сводке фрейма данных.) Аналогичным образом есть строка для «ТУРИСТИЧЕСКОЕ АГЕНТСТВО В ПАРКЕ», которое, согласно документации, было компанией, совладельцем которой является Enron. сестра бывшего председателя и явно не лицо, которое должно быть включено в набор данных.

Теперь я могу искать отдаленные точки данных, записанные в разных полях. Я постараюсь быть консервативным в отношении удаления выбросов, потому что набор данных довольно мал для машинного обучения в первую очередь. Более того, выбросы на самом деле могут быть важны, поскольку они могут представлять закономерности в данных, которые помогут идентифицировать интересующих людей. Официальное определение умеренного выброса либо ниже (первый квартиль минус 1,5-кратный межквартильный размах (IQR)), либо выше (третий квартиль плюс 1,5-кратный IQR):

низкий выброс ‹первый квартиль − 1,5 x IQR

высокий выброс ›третий квартиль + 1,5 * IQR

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

IQR = df.quantile(q=0.75) - df.quantile(q=0.25)
first_quartile = df.quantile(q=0.25)
third_quartile = df.quantile(q=0.75)
outliers = df[(df>(third_quartile + 1.5*IQR) ) | (df<(first_quartile - 1.5*IQR) )].count(axis=1)
outliers.sort_values(axis=0, ascending=False, inplace=True)
outliers.head(12)
Output:
LAY KENNETH L         15
FREVERT MARK A        12
BELDEN TIMOTHY N       9
SKILLING JEFFREY K     9
BAXTER JOHN C          8
LAVORATO JOHN J        8
DELAINEY DAVID W       7
KEAN STEVEN J          7
HAEDICKE MARK E        7
WHALLEY LAWRENCE G     7
RICE KENNETH D         6
KITCHEN LOUISE         6

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

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

  1. Кеннет Лэй, генеральный директор Enron в 1986–2001 годах, руководил многими видами незаконной коммерческой деятельности и, следовательно, является одним из наиболее важных лиц, представляющих интерес.
  2. Марк Фреверт занимал должность исполнительного директора Enron Europe с 1986 по 2000 год и был назначен председателем совета директоров Enron в 2001 году. Он был крупным игроком в фирме, хотя и не представлял интереса. Я считаю, что он не является представителем среднего сотрудника Enron в это время из-за его значительного вознаграждения и исключит его из набора данных.
  3. Тимоти Белден был бывшим руководителем торговых операций Enron, который разработал стратегию незаконного повышения цен на энергоносители в Калифорнии. Он был интересным человеком и обязательно останется в наборе данных.
  4. Джеффри Скиллинг сменил Кеннета Лэя на посту генерального директора Enron в 2001 году и организовал большую часть мошенничества, которое разрушило Enron. Как заинтересованное лицо он останется в наборе данных.
  5. Джон Бакстер был бывшим вице-президентом Enron и умер от очевидного выстрела самому себе, прежде чем он смог дать показания против других руководителей Enron. Я удалю его из набора данных, так как он не представляет интереса.
  6. Джон Лаворато был руководителем подразделения Enron по торговле энергоносителями и получал большие бонусы, чтобы удержать его от ухода из Enron. Поскольку он не представлял интереса, и большой бонус в конечном итоге исказил его общую зарплату, я думаю, было бы целесообразно удалить его из набора данных.
  7. Лоуренс Уолли занимал пост президента Enron и уволил Эндрю Фастоу, как только стало очевидно, насколько серьезна ситуация с Enron. Он был тщательно исследован, но не идентифицирован как лицо, представляющее интерес, и поэтому будет удален из набора данных.

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

# Remove the outlier individuals
df.drop(axis=0, labels=['FREVERT MARK A', 'LAVORATO JOHN J', 'WHALLEY LAWRENCE G', 'BAXTER JOHN C'], inplace=True)
# Find the number of poi and non poi now in the data
df['poi'].value_counts()
Output:
False    122
True      18

Теперь, когда очистка данных завершена, в наборе имеется в общей сложности 2800 наблюдений за финансовыми данными и данными электронной почты. Из них 1150 или 41% равны 0 для финансовых (платежных и складских) значений. Интересуют 18 человек, что составляет 12,9% физических лиц.

Начальное обучение алгоритму и показатели производительности

Первое обучение и тестирование, которые я проведу, будут касаться всех начальных функций в наборе данных. Это делается для того, чтобы оценить важность функций и служить базой для наблюдения за производительностью перед выбором каких-либо функций или настройкой параметров. Для начального тестирования я выбрал четыре алгоритма: Gaussian Naive Bayes (GaussianNB), DecisionTreeClassifier, Support Vector Classifier (SVC) и KMeans Clustering. Я буду запускать алгоритмы с параметрами по умолчанию, за исключением того, что я изменю ядро, используемое в машине опорных векторов, на линейное, и я выберу количество кластеров = 2 для KMeans, поскольку я заранее знаю, что цели - это только две категории, которые должны быть классифицирован. Хотя точность, по-видимому, является очевидным выбором для оценки качества классификатора, точность иногда может быть грубым показателем и не подходит для некоторых наборов данных, в том числе для Enron. Например, если классификатор предположит, что все образцы в очищенном наборе данных были не интересными людьми, его точность составит 87,1%. Однако это явно не удовлетворяет цели данного расследования, заключающейся в создании классификатора, который может идентифицировать лиц, представляющих интерес. Следовательно, необходимы разные показатели для оценки классификаторов и измерения производительности. Два выбранных для использования в этом проекте - Precision and Recall.

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

точность = истинные срабатывания / (истинные срабатывания + ложные срабатывания)

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

отзыв = истинные положительные результаты / (истинные положительные результаты + ложные отрицательные результаты)

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

Оценка F1 = 2 (точность x отзыв) / (точность + отзыв)

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

Масштабирование

Начальное обучение алгоритму и показатели производительности

Первое обучение и тестирование, которые я проведу, будут касаться всех начальных функций в наборе данных. Это делается для того, чтобы оценить важность функций и служить базой для наблюдения за производительностью перед выбором каких-либо функций или настройкой параметров. Для первоначального тестирования я выбрал четыре алгоритма: Gaussian Naive Bayes (GaussianNB), DecisionTreeClassifier, Support Vector Classifier (SVC) и KMeans Clustering. Я буду запускать алгоритмы с параметрами по умолчанию, за исключением того, что я изменю ядро, используемое в машине опорных векторов, на линейное, и я выберу количество кластеров = 2 для KMeans, поскольку я заранее знаю, что цели - это только две категории, которые должны быть классифицирован. Хотя точность может показаться очевидным выбором для оценки качества классификатора, точность иногда может быть грубым показателем и не подходит для некоторых наборов данных, в том числе для Enron. Например, если классификатор предположит, что все образцы в очищенном наборе данных были не интересными людьми, его точность составит 87,1%. Однако это явно не удовлетворяет цели данного расследования, заключающейся в создании классификатора, который может идентифицировать лиц, представляющих интерес. Следовательно, необходимы разные показатели для оценки классификаторов и измерения производительности. Для использования в этом проекте выбраны два варианта Precision and Recall.

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

точность = истинный положительный результат + ложный положительный результат точность = истинный положительный результат истинный положительный результат + ложный положительный результат

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

отзыв = истинно положительный результат + ложно отрицательный отзыв = истинный положительный результат истинный положительный результат + ложно отрицательный результат

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

Оценка F1 = 2 (точность x отзыв) точность + отзыв F1 Оценка = 2 (точность x отзыв) точность + отзыв

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

Масштабирование

Единственная подготовка данных, которую я сделаю для первоначального тестирования алгоритмов, - это масштабирование данных так, чтобы они имели нулевое среднее и единичную дисперсию. Этот процесс называется нормализацией и выполняется с помощью функции масштабирования из модуля предварительной обработки sklearn. Масштабирование в той или иной форме (будь то масштабирование MinMax или нормализация) обычно необходимо, потому что есть разные единицы для функций в наборе данных. Масштабирование создает безразмерные объекты, так что объекты с более крупными единицами измерения не оказывают чрезмерного влияния на классификатор, как это было бы в случае, если бы классификатор использовал какое-то измерение расстояния (например, евклидово расстояние) в качестве метрики сходства. Вот хорошее обсуждение масштабирования и нормализации функций.

from sklearn.preprocessing import scale
from sklearn.naive_bayes import GaussianNB
from sklearn.tree import DecisionTreeClassifier
from sklearn.svm import SVC
from sklearn.cluster import KMeans
from sklearn.ensemble import AdaBoostClassifier
import tester

# Scale the dataset and send it back to a dictionary
scaled_df = df.copy()
scaled_df.ix[:,1:] = scale(scaled_df.ix[:,1:])
my_dataset = scaled_df.to_dict(orient='index')

# Create and test the Gaussian Naive Bayes Classifier
clf = GaussianNB()
tester.dump_classifier_and_data(clf, my_dataset, features_list)
tester.main();
# Create and test the Decision Tree Classifier
clf = DecisionTreeClassifier()
tester.dump_classifier_and_data(clf, my_dataset, features_list)
tester.main();
# Create and test the Support Vector Classifier
clf = SVC(kernel='linear')
tester.dump_classifier_and_data(clf, my_dataset, features_list)
tester.main()
# Create and test the K Means clustering classifier
clf = KMeans(n_clusters=2)
tester.dump_classifier_and_data(clf, my_dataset, features_list)
tester.main();

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

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

Краткое примечание о проверке

Используемая здесь стратегия проверки - это форма перекрестной проверки, которая реализована в предоставленном скрипте tester.py. Перекрестная проверка выполняет несколько разделений набора данных, и в каждом из них формируется отдельный набор для обучения и тестирования. На каждой итерации классификатор помещается в обучающий набор, а затем тестируется на тестовом наборе. На следующей итерации классификатор снова обучается и тестируется, но на других наборах, и этот процесс продолжается для количества разбиений, сделанных из набора данных. Перекрестная проверка предотвращает классическую ошибку обучения алгоритма на тех же данных, которые использовались для тестирования алгоритма. Если это произойдет, результаты тестирования могут показать, что классификатор точен, но это только потому, что алгоритм уже видел данные тестирования раньше. Когда классификатор развертывается на новых образцах (реализованных в реальном мире), производительность может быть низкой, поскольку он был обучен и настроен для очень определенного набора экземпляров. Классификатор не сможет обобщить для новых случаев, потому что он подходит и настроен только для конкретных образцов, на которых он тестируется. Перекрестная проверка решает эту проблему путем обучения и тестирования нескольких различных подмножеств функций и меток и идеально подходит для использования с небольшими наборами данных, чтобы избежать переобучения. На протяжении всего анализа я использовал перекрестную проверку для оценки производительности моих алгоритмов. Скрипт tester.py использует метод перекрестной проверки StratifiedShuffleSplit, а GridSearchCV, который используется для поиска оптимального количества функций и лучших параметров, использует перекрестную проверку с перекрестной проверкой StratifiedKFolds. В обоих случаях набор данных 10 раз разделяется на обучающий и тестовый наборы.

Разработка функций

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

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

# Add the new email features to the dataframe
df['to_poi_ratio'] = df['from_poi_to_this_person'] / df['to_messages']
df['from_poi_ratio'] = df['from_this_person_to_poi'] / df['from_messages']
df['shared_poi_ratio'] = df['shared_receipt_with_poi'] / df['to_messages']

На этом этапе я также создам новые функции, используя финансовые данные. У меня есть несколько теорий, которые я сформировал на основе моего первоначального исследования данных и чтения о случае с Enron. Я думаю, что люди, получающие большие бонусы, могут быть более интересными, потому что бонусы могут быть результатом мошенничества. Было бы легче выдать незаконные средства за премию, чем за повышение зарплаты, которое обычно связано с контрактом и участием акционеров. Две новые функции будут бонусом к зарплате и бонусом к общей сумме выплат. В настоящее время существует 25 функций, некоторые из которых, скорее всего, избыточны или не представляют никакой ценности. Я выполню сокращение / выбор функций, чтобы оптимизировать количество функций, поэтому меня не беспокоит первоначальное большое количество функций. Более того, используемые мной алгоритмы могут относительно быстро обучаться даже при большом количестве функций, поскольку общее количество выборок данных невелико.

# Create the new financial features and add to the dataframe
df['bonus_to_salary'] = df['bonus'] / df['salary']
df['bonus_to_total'] = df['bonus'] / df['total_payments']

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

Оценка F1 для дерева решений по-прежнему самая высокая, за ней следует гауссовский наивный байесовский классификатор. На данный момент ни SVC с линейным ядром, ни кластеризация KMeans не соответствуют стандарту 0,3 для точности и отзыва. Я откажусь от двух последних алгоритмов, а также от GaussianNB в пользу AdaBoost Classifier, потому что я хочу поэкспериментировать с настраиваемыми параметрами, а у GaussianNB их нет. AdaBoost берет слабый классификатор и несколько раз обучает его на наборе данных, при каждом запуске корректируя веса неправильно классифицированных экземпляров, чтобы сосредоточиться на наиболее сложных для классификации выборках. Таким образом, AdaBoost работает для итеративного улучшения существующего классификатора и может использоваться в сочетании с DecisionTree или GaussianNB.

Визуализация функций

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

График ниже представляет собой матрицу разброса, показывающую все отношения между четырьмя выбранными функциями: «бонус», «total_payments», «to_poi_ratio» и «from_poi_ratio». Диагонали - это гистограммы, потому что переменная, коррелированная сама с собой, равна единице. Интересующие лица показаны желтым цветом, а лица, не представляющие интереса, - фиолетовыми точками. В целом, похоже, есть несколько тенденций. Глядя на бонус vs from_poi_ratio, интересующие лица обычно располагаются дальше вправо и выше, чем неинтересные лица. Это указывает на то, что заинтересованные лица, как правило, получают более крупные бонусы и отправляют больше электронных писем другим заинтересованным лицам. Шкала на графике общих выплат затрудняет чтение этих графиков, но видно, что у лиц, представляющих интерес, как правило, также более высокие общие выплаты. Эту тенденцию можно увидеть на графике разброса to_poi_ratio против total_payments. Возможно, это демонстрирует, что заинтересованные лица получают больше общих выплат из-за мошенничества или потому, что они, как правило, работают на высшем уровне в компании и, следовательно, получают более высокую заработную плату.

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

Выбор функций

Существует несколько методов выбора функций в машинном обучении. Один из них - просто посмотреть на важность функций для классификатора и изменить список функций, чтобы исключить те, которые имеют важность ниже выбранного порогового значения. Другой - использовать SelectKBest и автоматически выбирать k-лучшие функции, определяемые величиной объясненной дисперсии для использования в классификаторе. Я рассмотрю важность функций как для DecisionTree, так и для классификатора AdaBoost, но я бы предпочел использовать SelectKBest для фактического выбора функций, которые нужно сохранить. Кроме того, я могу использовать GridSearchCV в сочетании с SelectKBest, чтобы найти оптимальное количество функций для использования. При этом будет пройдено несколько значений k и выбрано то, которое дает наибольшее значение в соответствии с назначенной метрикой производительности.

Во-первых, я вручную посмотрю на важность функций для обоих классификаторов, чтобы понять, какие функции наиболее важны. Одним из важных аспектов машинного обучения является то, что оно помогает людям мыслить умнее. Посмотрев на то, что алгоритм выбирает в качестве наиболее важных функций для идентификации лиц, представляющих интерес, он может информировать людей о том, что им следует искать в аналогичных случаях. (Один из примеров этого в реальном мире - когда AlphaGo от Google победила Ли Седола в игре Go, она продвинула все состояние го, предоставив новые идеи и стратегии в игру, в которую люди играли тысячелетиями.)

# Get the feature importances of the DecisionTree Classifier
tree_feature_importances = (clf_tree.feature_importances_)
tree_features = zip(tree_feature_importances, features_list[1:])
tree_features = sorted(tree_features, key= lambda x:x[0], reverse=True)

# Display the feature names and importance values
print('Tree Feature Importances:\n')
for i in range(10):
    print('{} : {:.4f}'.format(tree_features[i][1], tree_features[i][0]))
Output:
Tree Feature Importances:

from_poi_ratio : 0.3782
shared_receipt_with_poi : 0.2485
expenses : 0.2476
shared_poi_ratio : 0.0665
from_poi_to_this_person : 0.0592
salary : 0.0000
bonus : 0.0000
long_term_incentive : 0.0000
deferred_income : 0.0000
deferral_payments : 0.0000

Найдите важность функций для классификатора AdaBoost

# Get the feature importances for the AdaBoost Classifier
ada_feature_importances = clf_ada.feature_importances_
ada_features = zip(ada_feature_importances, features_list[1:])

# Display the feature names and importance values
print('Ada Boost Feature Importances:\n')
ada_features = sorted(ada_features, key=lambda x:x[0], reverse=True)
for i in range(10):
    print('{} : {:.4f}'.format(ada_features[i][1], ada_features[i][0]))
Output:
Ada Boost Feature Importances:

shared_receipt_with_poi : 0.1200
exercised_stock_options : 0.1000
from_this_person_to_poi : 0.1000
to_poi_ratio : 0.1000
deferred_income : 0.0800
from_messages : 0.0800
from_poi_ratio : 0.0800
other : 0.0600
total_stock_value : 0.0600
shared_poi_ratio : 0.0400

Интересно сравнить важность функций для классификаторов DecisionTree и AdaBoost. Топ-10 характеристик не полностью совпадают, хотя оба классификатора достигли респектабельной оценки F1 выше 0,5. Однако вместо того, чтобы вручную выбирать функции, которые нужно сохранить, я буду использовать GridSearchCV с SelectKBest, чтобы найти оптимальное количество функций для классификаторов. GridSearchCV проходит через сетку параметров и тестирует все различные конфигурации, предоставленные ей. Он возвращает параметры, которые дают максимальное количество баллов. Я буду использовать параметр оценки F1, потому что это то, что я хотел бы максимизировать, и перекрестную проверку с 10 разбиениями, чтобы убедиться, что я не переоснащаю алгоритм для обучающих данных.

from sklearn.model_selection import GridSearchCV

n_features = np.arange(1, len(features_list))

# Create a pipeline with feature selection and classification
pipe = Pipeline([
    ('select_features', SelectKBest()),
    ('classify', DecisionTreeClassifier())
])

param_grid = [
    {
        'select_features__k': n_features
    }
]

# Use GridSearchCV to automate the process of finding the optimal number of features
tree_clf= GridSearchCV(pipe, param_grid=param_grid, scoring='f1', cv = 10)
tree_clf.fit(features, labels);

Согласно поиску по сетке, выполненному с помощью SelectKBest с количеством функций от 1 до 24 (количество функций минус одна), оптимальное количество функций для классификатора дерева решений составляет 19. Я могу посмотреть оценки, присвоенные наиболее эффективным функциям, с помощью атрибута scores в SelectKBest.

SelectKBest по умолчанию использует параметры оценки с использованием F-значения ANOVA, которое является мерой вариации между средними значениями выборки. Он описывает, насколько разница между ярлыками объясняется конкретной особенностью. Следовательно, более высокое значение означает, что существует больше различий в этой характеристике между лицом, представляющим интерес, и лицом, не представляющим интерес. В следующей таблице приведены функции дерева решений и F-значение ANOVA, возвращаемое SelectKBest с k = 19. Это функции, которые я использовал в моем последнем DecisionTreeClassifier.

Запуск DecisionTreeClassifier с SelectKMeans и k = 19 дает оценку F1 0,700. Я очень доволен этим результатом и решил, что буду использовать 19 функций с DecisionTreeClassifier. Любые дальнейшие улучшения этого классификатора будут внесены в раздел исследования, посвященный настройке параметров.

Аналогичная процедура с GridSearchCV и SelectKBest будет выполнена для определения оптимального количества функций для использования с AdaBoostClassifier.

n_features = np.arange(1, len(features_list))
# Create a pipeline of feature selection and classifier
pipe = Pipeline([
    ('select_features', SelectKBest()),
    ('classify', AdaBoostClassifier())
])

param_grid = [
    {
        'select_features__k': n_features
    }
]

# Use GridSearchCV to automate the process of finding the optimal number of features
ada_clf= GridSearchCV(pipe, param_grid=param_grid, scoring='f1', cv =10)
ada_clf.fit(features, labels)

Идеальное количество параметров для SelectKMeans, использующего GridSearch для AdaBoostClassifier, было 23. Это привело к немного более низкому баллу F1 - 0,689, и я буду использовать 23 функции с наивысшим баллом с AdaBoostClassifier. Результаты остались прежними, поэтому я не буду показывать их все снова.

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

Оптимизация гиперпараметров

Настройка параметров - это процесс оптимизации настроек алгоритма машинного обучения для достижения максимальной производительности на заданном наборе данных. Алгоритм машинного обучения - это просто последовательность правил, которые компьютер применяет к набору функций для получения классификации. Настройку параметров можно рассматривать как итеративное изменение этих правил для получения лучших классификаций. Sci-kit Learn реализует параметры по умолчанию для каждого алгоритма, предназначенного для запуска и работы классификатора, путем обобщения на как можно большее количество наборов данных с приличной производительностью. Однако можно добиться большей производительности, изменив эти параметры алгоритма (хотя они часто используются взаимозаменяемо, технически гиперпараметры - это аспекты оценщика, которые должны быть заранее установлены пользователем, в то время как параметры модели узнаются из набора данных. ; См. Обсуждение). Процесс настройки параметров для набора данных может выполняться вручную, путем выбора различных конфигураций, выполнения перекрестной проверки и выбора параметров, обеспечивающих максимальную производительность, или его можно автоматизировать с помощью другого алгоритма, такого как GridSearchCV. Сетка параметров передается в GridSearchCV, которая состоит из ряда комбинаций параметров для тестирования с помощью алгоритма, и поиск возвращает параметры, которые максимизируют производительность. Параметры, доступные для алгоритма в sci-kit learn, можно найти в документации классификатора. Мой процесс настройки алгоритма будет заключаться в изучении документации научного набора, построении сетки параметров с широким диапазоном конфигураций и использовании GridSearchCV для поиска оптимальных настроек.

Дерево решений будет первым. Глядя на документацию для DecisionTreeClassifier из научного набора, можно увидеть множество параметров, которые можно изменить, каждый из которых изменяет способ, которым алгоритм принимает решения, когда он анализирует функции и создает дерево. Например, критерий может быть установлен как Джини или энтропия, который определяет, как алгоритм измеряет качество разделения узла, или, другими словами, какую ветвь дерева выбрать. чтобы прийти к правильной классификации. ('gini' использует примесь джини, в то время как 'энтропия' максимизирует [получение информации] при каждом ветвлении (https://en.wikipedia.org/wiki/Information_gain_ratio). Я помещу оба варианта в свою сетку параметров и позвольте поиску по сетке определить, какой из них лучше. Три других параметра, которые я настрою, - это min_samples_split, max_depth и max_features. GridSearch будет направлен путем перекрестной проверки с 10 разделениями данных, а критерий оценки указан как F1, потому что это основная мера производительности классификатора, которую я использовал для учета как точности, так и отзыва.

# Create a pipeline with feature selection and classifier
tree_pipe = Pipeline([
    ('select_features', SelectKBest(k=19)),
    ('classify', DecisionTreeClassifier()),
])

# Define the configuration of parameters to test with the 
# Decision Tree Classifier
param_grid = dict(classify__criterion = ['gini', 'entropy'] , 
                  classify__min_samples_split = [2, 4, 6, 8, 10, 20],
                  classify__max_depth = [None, 5, 10, 15, 20],
                  classify__max_features = [None, 'sqrt', 'log2', 'auto'])

# Use GridSearchCV to find the optimal hyperparameters for the classifier
tree_clf = GridSearchCV(tree_pipe, param_grid = param_grid, scoring='f1', cv=10)
tree_clf.fit(features, labels)
# Get the best algorithm hyperparameters for the Decision Tree
tree_clf.best_params_
Output:
{'classify__criterion': 'entropy',
 'classify__max_depth': None,
 'classify__max_features': None,
 'classify__min_samples_split': 20}

Наилучшие выявленные параметры показаны выше. Использование GridSearch избавило меня от утомительной работы по поиску и оценке вручную всех комбинаций гиперпараметров. Теперь я буду реализовывать лучшие параметры и оценивать классификатор дерева решений, используя перекрестную проверку, доступную в функции tester.py.

# Create the classifier with the optimal hyperparameters as found by GridSearchCV
tree_clf = Pipeline([
    ('select_features', SelectKBest(k=20)),
    ('classify', DecisionTreeClassifier(criterion='entropy', max_depth=None, max_features=None, min_samples_split=20))
])

# Test the classifier using tester.py
tester.dump_classifier_and_data(tree_clf, my_dataset, features_list)
tester.main()

Согласно перекрестной проверке в tester.py, мой результат F1 составляет около 0,800 при оптимальных параметрах. Я удовлетворен оценкой отзыва и точности DecisionTreeClassifier, но я попробую AdaBoostClassifier, используя тот же подход, потому что мне любопытно посмотреть, смогу ли я превзойти оценку F1.

Пришло время взглянуть на Sci-kit learn документацию по классификатору AdaBoost, чтобы увидеть параметры, доступные для настройки. AdaBoostClassifier усиливает другой базовый классификатор, которым по умолчанию является Дерево решений. Я могу изменить это, используя параметр base_estimator, чтобы проверить случайный лес и гауссовскую наивную классификацию Байеса. Другими параметрами, которые я могу изменить, являются n_estimators - количество подходящих слабых моделей и скорость обучения - мера веса, присвоенного каждому классификатору. AdaBoost обычно используется со слабыми классификаторами или с классификаторами, которые работают лишь немного лучше, чем случайные. Одним из примеров может быть пень решения или дерево решений только с одним слоем. Теоретически классификатор AdaBoost должен работать лучше, чем дерево решений, потому что он запускает дерево решений несколько раз и итеративно корректирует веса, присвоенные каждой функции, чтобы делать более точные прогнозы. Однако на практике более сложный алгоритм не всегда работает лучше, чем простая модель, и, в конце концов, более качественные данные будут превосходить тщательно настроенный алгоритм.

# Create the pipeline with feature selection and AdaBoostClassifier
ada_pipe = Pipeline([('select_features', SelectKBest(k=20)),
                     ('classify', AdaBoostClassifier())
                    ])

# Define the parameter configurations to test with GridSearchCV
param_grid = dict(classify__base_estimator=[DecisionTreeClassifier(), RandomForestClassifier(), GaussianNB()],
                  classify__n_estimators = [30, 50, 70, 120],
                  classify__learning_rate = [0.5, 1, 1.5, 2, 4])

# Use GridSearchCV to automate the process of finding the optimal parameters
ada_clf = GridSearchCV(ada_pipe, param_grid=param_grid, scoring='f1', cv=10)
ada_clf.fit(features, labels)
# Display the best parameters for the AdaBoostClassifier
ada_clf.best_params_
Output:
{'classify__base_estimator': DecisionTreeClassifier(class_weight=None, criterion='gini', max_depth=None,
             max_features=None, max_leaf_nodes=None,
             min_impurity_split=1e-07, min_samples_leaf=1,
             min_samples_split=2, min_weight_fraction_leaf=0.0,
             presort=False, random_state=None, splitter='best'),
 'classify__learning_rate': 1,
 'classify__n_estimators': 70}

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

Полученные результаты

# Implement the Decision Tree Classifier with the optimal parameters
tree_clf = Pipeline([
    ('select_features', SelectKBest(k=19)),
    ('classify', DecisionTreeClassifier(criterion='entropy', max_depth=None, max_features=None, min_samples_split=20))
])

# Test the classifier with cross-validation
tester.dump_classifier_and_data(tree_clf, my_dataset, features_list)
tester.main()
# Implement the AdaBoost Classifier with the optimal parameters
ada_clf = Pipeline([('select_features', SelectKBest(k=23)),
                   ('classify', AdaBoostClassifier(base_estimator=DecisionTreeClassifier(), learning_rate=1, n_estimators=70))
                   ])

# Test the classifier with cross-validation
tester.dump_classifier_and_data(ada_clf, my_dataset, features_list)
tester.main()

Результаты выполнения финальных версий алгоритмов показаны ниже:

Заключение

Хотя сценарий, который я использовал для тестирования классификатора, реализовал перекрестную проверку, я скептически относился к зарегистрированным относительно высоким показателям точности, отзывчивости и F1. Я осознавал, что каким-то образом перестроил свою модель под данные, хотя скрипт реализует перекрестную проверку. Просматривая сценарий tester.py, я увидел, что случайное начальное число для разделения перекрестной проверки было установлено на 42, чтобы генерировать воспроизводимые результаты. Я изменил случайное начальное число, и, конечно же, производительность моей модели снизилась. Следовательно, я, должно быть, совершил классическую ошибку, переобучив свой обучающий набор для данного случайного числа с перекрестной проверкой, и мне нужно будет искать эту проблему в будущем. Даже принимая меры против переобучения, я все же оптимизировал свою модель для определенного набора данных. Чтобы получить лучший индикатор производительности модели дерева решений, я провел 10 тестов с разными случайными начальными числами и нашел средние показатели производительности. Окончательные результаты для моей модели приведены ниже:

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

Оценка точности 0,782 означает, что из лиц, обозначенных в моей модели как лиц, представляющих интерес, 78,2% действительно были лицами, представляющими интерес. Оценка запоминания 0,820 означает, что моя модель определила 82,0% заинтересованных лиц, присутствующих во всем наборе данных.

Главный вывод из этого проекта заключался в важности качественных данных по сравнению с точной настройкой алгоритма. Инженерия функций за счет создания новых функций и выбора тех, которые имеют наибольшую объясненную дисперсию, увеличила оценку F1 классификатора с ~ 0,40 до ~ 0,70. Однако настройка гиперпараметров алгоритма только увеличила оценку F1 до 0,80. Впоследствии, при разработке будущих моделей машинного обучения, я сосредоточусь на сборе как можно большего количества высококачественных данных, прежде чем я даже подумаю об обучении и настройке алгоритма. Этот момент был разъяснен в статье Алона Халеви и Питера Норвига 2009 года, озаглавленной Неоправданная эффективность данных. Главный аргумент статьи заключается в том, что по мере увеличения количества достоверных данных выбор алгоритма имеет все меньшее и меньшее значение, и даже самый простой алгоритм может соответствовать производительности наиболее хорошо настроенного алгоритма. Более того, этот проект показал мне, что человеческая интуиция относительно ситуации не всегда будет соответствовать результатам, полученным с помощью модели машинного обучения. Например, гипотеза Ими заключалась в том, что наиболее предсказуемыми индикаторами мошеннической деятельности были бы электронные письма, отправленные / от заинтересованных лиц. Использование автоматического выбора функций показало, что порядок важности был бонусом, from_poi_ratio, salary, total_stock_value. Моя интуиция была частично верной, но у алгоритма были и другие (и более точные) идеи о том, что имеет значение, когда дело доходит до идентификации интересующих людей. Тем не менее, это несоответствие между моим мышлением и классификатором дает возможность поучиться у машины и обновить мое мышление. В конечном счете, я вижу важность машинного обучения не в передаче всех решений алгоритмам, а в использовании машин для обработки и получения информации на основе данных, которые могут способствовать более разумному мышлению и позволяют людям создавать более эффективные системы.

Приложение

tester.py Предварительная функция, предоставляемая Udacity.

"" a basic script for importing student's POI identifier,
    and checking the results that they get from it 
 
    requires that the algorithm, dataset, and features list
    be written to my_classifier.pkl, my_dataset.pkl, and
    my_feature_list.pkl, respectively
    that process should happen at the end of poi_id.py
"""
import pickle
import sys
from sklearn.cross_validation import StratifiedShuffleSplit
sys.path.append("../tools/")
from feature_format import featureFormat, targetFeatureSplit
PERF_FORMAT_STRING = "\
\tAccuracy: {:>0.{display_precision}f}\tPrecision: {:>0.{display_precision}f}\t\
Recall: {:>0.{display_precision}f}\tF1: {:>0.{display_precision}f}\tF2: {:>0.{display_precision}f}"
RESULTS_FORMAT_STRING = "\tTotal predictions: {:4d}\tTrue positives: {:4d}\tFalse positives: {:4d}\
\tFalse negatives: {:4d}\tTrue negatives: {:4d}"
def test_classifier(clf, dataset, feature_list, folds = 1000):
    data = featureFormat(dataset, feature_list, sort_keys = True)
    labels, features = targetFeatureSplit(data)
    cv = StratifiedShuffleSplit(labels, folds, random_state = 42)
    true_negatives = 0
    false_negatives = 0
    true_positives = 0
    false_positives = 0
    for train_idx, test_idx in cv: 
        features_train = []
        features_test  = []
        labels_train   = []
        labels_test    = []
        for ii in train_idx:
            features_train.append( features[ii] )
            labels_train.append( labels[ii] )
        for jj in test_idx:
            features_test.append( features[jj] )
            labels_test.append( labels[jj] )
        
        ### fit the classifier using training set, and test on test set
        clf.fit(features_train, labels_train)
        predictions = clf.predict(features_test)
        for prediction, truth in zip(predictions, labels_test):
            if prediction == 0 and truth == 0:
                true_negatives += 1
            elif prediction == 0 and truth == 1:
                false_negatives += 1
            elif prediction == 1 and truth == 0:
                false_positives += 1
            elif prediction == 1 and truth == 1:
                true_positives += 1
            else:
                print "Warning: Found a predicted label not == 0 or 1."
                print "All predictions should take value 0 or 1."
                print "Evaluating performance for processed predictions:"
                break
    try:
        total_predictions = true_negatives + false_negatives + false_positives + true_positives
        accuracy = 1.0*(true_positives + true_negatives)/total_predictions
        precision = 1.0*true_positives/(true_positives+false_positives)
        recall = 1.0*true_positives/(true_positives+false_negatives)
        f1 = 2.0 * true_positives/(2*true_positives + false_positives+false_negatives)
        f2 = (1+2.0*2.0) * precision*recall/(4*precision + recall)
        print clf
        print PERF_FORMAT_STRING.format(accuracy, precision, recall, f1, f2, display_precision = 5)
        print RESULTS_FORMAT_STRING.format(total_predictions, true_positives, false_positives, false_negatives, true_negatives)
        print ""
    except:
        print "Got a divide by zero when trying out:", clf
        print "Precision or recall may be undefined due to a lack of true positive predicitons."
CLF_PICKLE_FILENAME = "my_classifier.pkl"
DATASET_PICKLE_FILENAME = "my_dataset.pkl"
FEATURE_LIST_FILENAME = "my_feature_list.pkl"
def dump_classifier_and_data(clf, dataset, feature_list):
    with open(CLF_PICKLE_FILENAME, "w") as clf_outfile:
        pickle.dump(clf, clf_outfile)
    with open(DATASET_PICKLE_FILENAME, "w") as dataset_outfile:
        pickle.dump(dataset, dataset_outfile)
    with open(FEATURE_LIST_FILENAME, "w") as featurelist_outfile:
        pickle.dump(feature_list, featurelist_outfile)
def load_classifier_and_data():
    with open(CLF_PICKLE_FILENAME, "r") as clf_infile:
        clf = pickle.load(clf_infile)
    with open(DATASET_PICKLE_FILENAME, "r") as dataset_infile:
        dataset = pickle.load(dataset_infile)
    with open(FEATURE_LIST_FILENAME, "r") as featurelist_infile:
        feature_list = pickle.load(featurelist_infile)
    return clf, dataset, feature_list
def main():
    ### load up student's classifier, dataset, and feature_list
    clf, dataset, feature_list = load_classifier_and_data()
    ### Run testing script
    test_classifier(clf, dataset, feature_list)
if __name__ == '__main__':
    main()