В этом блоге рассказывается о применении глубокого обучения для структурированных данных. Я буду объяснять предобработку кода из курса fast.ai Deep Learning, часть 1, Урок-4, Блокнот Россманна.

Для совместной работы ноутбука необходимо установить следующие версии «fastai» и «torchtext»,

!pip install fastai==0.7.0
!pip install torchtext==0.2.3

Имея такую ​​настройку, мы можем без проблем запускать блокнот в Google Collab. Поддерживаемые версии меняются так быстро, что к моменту запуска ноутбука вы можете столкнуться с проблемами. В этом случае оставьте комментарий к своей проблеме ниже, я вам помогу.

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

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

Есть и другие очень хорошие блоги от @timlee и @hiromi_suenaga, которые очень хорошо написали об этом блокноте. Их основное внимание было сосредоточено на объяснении части Deep Learning, они вкратце коснулись части предварительной обработки реализации.

Эта статья будет больше сосредоточена на объяснении части предварительной обработки кода и очень кратко на части кода DL. Я постараюсь изо всех сил объяснить ту часть кода, которую мне трудно понять, и пропущу простые части.

Основные этапы предварительной обработки:

  1. Создать набор данных
  2. Очистка данных / Разработка функций
  3. Продолжительность относительно столбца "Дата-время"

Создание набора данных:

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

table_names = [‘train’, ‘store’, ‘store_states’, ‘state_names’, 
 ‘googletrend’, ‘weather’, ‘test’]

Помимо исходных таблиц, у нас есть store_states, state_names, googletrend, weather, которые используются для включения дополнительных функций. Эти дополнительные таблицы предоставят больше возможностей, которые могут повлиять на процесс обучения путем обобщения модели. Чем выше количество рассматриваемых важных функций, тем лучше будет аппроксимирующая способность нашей модели DL.

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

Рекомендуется поискать как можно больше таблиц, которые могут повлиять на продажи. Сложная часть состоит в том, чтобы собрать их в одну таблицу и передать ее в качестве входных данных для модели DL. Блокнот fast.ai очень хорошо это сделал.

tables = [pd.read_csv(f’{PATH}{fname}.csv’, low_memory=False) for fname in table_names]
for t in tables: display(t.head())

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

Очистка данных / разработка функций

Следующие 10 шагов объясняют работу записной книжки, от предварительной обработки до модели DL:

1. Замена двоичных категорий на логический тип dtype, с которым намного удобнее работать.

train.StateHoliday = train.StateHoliday!=’0'
test.StateHoliday = test.StateHoliday!=’0'

2. Объединение таблиц по одной с помощью оператора слияния панд. В итоге у нас должна остаться одна таблица, к которой мы применим модель DL.

«Join_df» - это функция, которая объединяет две таблицы. Он использует оператор слияния pandas с внешним соединением для выполнения этой работы. Внешнее соединение предпочтительнее внутреннего соединения, потому что оно покажет неперекрывающиеся строки в результирующей таблице. С пропущенными значениями как «NaN». Этот набор данных структурирован так, что после объединения таблиц отсутствуют пропущенные значения.

Это также гарантирует, что пропущенные значения проверяются на их наличие после объединения таблиц.

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

def join_df(left, right, left_on, right_on=None, suffix=’_y’):
 if right_on is None: right_on = left_on
 return left.merge(right, how=’left’, left_on=left_on, right_on=right_on, 
 suffixes=(“”, suffix))
weather = join_df(weather, state_names, “file”, “StateName”)
googletrend[‘Date’] = googletrend.week.str.split(‘ — ‘, expand=True)[0]
googletrend[‘State’] = googletrend.file.str.split(‘_’, expand=True)[2]
googletrend.loc[googletrend.State==’NI’, “State”] = ‘HB,NI’
trend_de = googletrend[googletrend.file == ‘Rossmann_DE’]
store = join_df(store, store_states, “Store”)
len(store[store.State.isnull()])
joined = join_df(train, store, “Store”)
len(joined[joined.StoreType.isnull()])
joined = join_df(joined, googletrend, [“State”,”Year”, “Week”])
len(joined[joined.trend.isnull()])
joined = joined.merge(trend_de, ‘left’, [“Year”, “Week”], suffixes=(‘’, ‘_DE’))
len(joined[joined.trend_DE.isnull()])
joined = join_df(joined, weather, [“State”,”Date”])
len(joined[joined.Mean_TemperatureC.isnull()])
joined_test = test.merge(store, how=’left’, left_on=’Store’, right_index=True)
len(joined_test[joined_test.StoreType.isnull()])
for c in joined.columns:
 if c.endswith(‘_y’):
 if c in joined.columns: joined.drop(c, inplace=True, axis=1)

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

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

«add_datepart» - это функция fast.ai, выполняющая эту работу. Например, время от последнего промо, тайл, оставшийся до следующего промо, день недели и т. Д., Которые играют важную роль в прогнозировании продаж.

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

add_datepart(weather, “Date”, drop=False)
add_datepart(googletrend, “Date”, drop=False)
add_datepart(train, “Date”, drop=False)
add_datepart(test, “Date”, drop=False)
add_datepart(googletrend, “Date”, drop=False)

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

«fillna ()» - это функция pandas, которая заполняет отсутствующие значения значениями по нашему выбору. Ниже представлен случай года, когда пропущенные значения заполняются 1900 годом, «CompetitionOpenSinceMonth», «Promo2SinceWeek» заполняются значением один.

Extreme values are replaced by a cut off minimum and maximum values, using pandas filter technique.
joined.CompetitionOpenSinceYear = joined.CompetitionOpenSinceYear.fillna(1900).astype(np.int32)
joined.CompetitionOpenSinceMonth = joined.CompetitionOpenSinceMonth.fillna(1).astype(np.int32)
joined.Promo2SinceYear = joined.Promo2SinceYear.fillna(1900).astype(np.int32)
joined.Promo2SinceWeek = joined.Promo2SinceWeek.fillna(1).astype(np.int32)
joined[“CompetitionOpenSince”] = pd.to_datetime(dict(year=joined.CompetitionOpenSinceYear, 
 month=joined.CompetitionOpenSinceMonth, day=15))
joined[“CompetitionDaysOpen”] = joined.Date.subtract(joined.CompetitionOpenSince).dt.days
joined.loc[joined.CompetitionDaysOpen<0, “CompetitionDaysOpen”] = 0
joined.loc[joined.CompetitionOpenSinceYear<1990, “CompetitionDaysOpen”] = 0
joined[“CompetitionMonthsOpen”] = joined[“CompetitionDaysOpen”]//30
joined.loc[joined.CompetitionMonthsOpen>24, “CompetitionMonthsOpen”] = 24
joined.CompetitionMonthsOpen.unique()
joined[“Promo2Since”] = pd.to_datetime(joined.apply(lambda x: Week(
 x.Promo2SinceYear, x.Promo2SinceWeek).monday(), axis=1).astype(pd.datetime))
joined[“Promo2Days”] = joined.Date.subtract(joined[“Promo2Since”]).dt.days
joined.loc[joined.Promo2Days<0, “Promo2Days”] = 0
joined.loc[joined.Promo2SinceYear<1990, “Promo2Days”] = 0
joined[“Promo2Weeks”] = joined[“Promo2Days”]//7
joined.loc[joined.Promo2Weeks<0, “Promo2Weeks”] = 0
joined.loc[joined.Promo2Weeks>25, “Promo2Weeks”] = 25
joined.Promo2Weeks.unique()
joined.to_feather(f’{PATH}joined’)

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

cat_vars = [‘Store’, ‘DayOfWeek’, ‘Year’, ‘Month’, ‘Day’, ‘StateHoliday’, ‘CompetitionMonthsOpen’,
 ‘Promo2Weeks’, ‘StoreType’, ‘Assortment’, ‘PromoInterval’, ‘CompetitionOpenSinceYear’, ‘Promo2SinceYear’,
 ‘State’, ‘Week’, ‘Events’, ‘Promo_fw’, ‘Promo_bw’, ‘StateHoliday_fw’, ‘StateHoliday_bw’,
 ‘SchoolHoliday_fw’, ‘SchoolHoliday_bw’]
contin_vars = [‘CompetitionDistance’, ‘Max_TemperatureC’, ‘Mean_TemperatureC’, ‘Min_TemperatureC’,
 ‘Max_Humidity’, ‘Mean_Humidity’, ‘Min_Humidity’, ‘Max_Wind_SpeedKm_h’, 
 ‘Mean_Wind_SpeedKm_h’, ‘CloudCover’, ‘trend’, ‘trend_DE’,
 ‘AfterStateHoliday’, ‘BeforeStateHoliday’, ‘Promo’, ‘SchoolHoliday’]
n = len(joined); n

Следующие строки кода гарантируют, что те столбцы, которые указаны в категории, будут преобразованы в тип «категория» с функциями панд. Те, которые являются «непрерывными», получают тип «float32», который является стандартом Pytorch для данных с плавающей запятой.

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

for v in cat_vars: joined[v] = joined[v].astype(‘category’).cat.as_ordered()
for v in contin_vars: joined[v] = joined[v].astype(‘float32’)
dep = ‘Sales’
joined = joined[cat_vars+contin_vars+[dep, ‘Date’]]

7. Выберите подмножество (150000) данных поезда. Примените «proc_df» подмножества.

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

idxs = get_cv_idxs(n, val_pct=150000/n)
joined_samp = joined.iloc[idxs].set_index(“Date”)
samp_size = len(joined_samp); samp_size

«proc_df» выполняет следующие действия:

  1. proc_df - вытягивает цель (y) (продажи) и удаляет из оригинала.
  2. Также масштабирует dataFrame (do_scale = True).
  3. Также создает другой объект для отслеживания стандартного и среднего значения для изменения набора тестов.
  4. Также обрабатывает пропущенные значения, заполняет медианой.
df, y, nas, mapper = proc_df(joined_samp, ‘Sales’, do_scale=True)
yl = np.log(y)

8. Разделите образцы данных на наборы данных для обучения и проверки.

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

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

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

train_ratio = 0.75
train_size = int(samp_size * train_ratio); train_size
val_idx = list(range(train_size, len(df)))

9. Создание вложений для категориальных переменных.

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

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

Вторая строка кода ниже использует деление пола для определения длины вектора.

cat_sz = [(c, len(joined_samp[c].cat.categories)+1) for c in cat_vars]
emb_szs = [(c, min(50, (c+1)//2)) for _,c in cat_sz]

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

10. Теперь данные готовы к вводу в модель DL.

Теперь у нас есть DataFrame (df) и целевые значения (yl) с четким указанием того, какие столбцы являются категориальными и непрерывными. Используя эту информацию, мы создаем объект данных модели «md», который можно увидеть в строке кода ниже. Это функциональность fast.ai, оболочка, написанная поверх Pytorch.

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

md = ColumnarModelData.from_data_frame(PATH, val_idx, df, yl, cat_flds=cat_vars, bs=128)

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

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

Эта строка кода создает объект модели. Его параметрами являются все векторы внедрения (emb_szs), количество непрерывных переменных, количество скрытых слоев и нейронов на слой [1000,500], выпадение при внедрении слоя (0,04), выпадение на уровне [0,001,0,01], целевые метки (диапазон y).

m = md.get_learner(emb_szs, len(df.columns)-len(cat_vars),
 0.04, 1, [1000,500], [0.001,0.01], y_range=y_range)

Последним шагом является следующее: данные соответствуют модели и начинается обучение. Он учитывает скорость обучения «lr», количество используемых эпох и метрик. Используемый здесь показатель - RMSPE (среднеквадратичная ошибка в процентах). Это метрика, используемая в расчетах ошибок Kaggle.

m.fit(lr, 3, metrics=[exp_rmspe])
def inv_y(a): return np.exp(a)
def exp_rmspe(y_pred, targ):
 targ = inv_y(targ)
 pct_var = (targ — inv_y(y_pred))/targ
 return math.sqrt((pct_var**2).mean())
max_log_y = np.max(yl)
y_range = (0, max_log_y*1.2)

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

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

  1. join_df

Обратитесь к пункту 2 для подробного объяснения этой функции.

2. add_datepart

См. пункт 4 для подробного объяснения этой функции.

3. get_elapsed

Эта функция принимает «поле» и другой параметр, который имеет два варианта: «До» и «После». В зависимости от того, какой параметр мы выбираем, он сообщает нам, произошло ли событие в поле «После» или «До» сколько дней. Для получения этой информации используется столбец времени / даты.

def get_elapsed(fld, pre):
 day1 = np.timedelta64(1, ‘D’)
 last_date = np.datetime64()
 last_store = 0
 res = []
for s,v,d in zip(df.Store.values,df[fld].values, df.Date.values):
 if s != last_store:
 last_date = np.datetime64()
 last_store = s
 if v: last_date = d
 res.append(((d-last_date).astype(‘timedelta64[D]’) / day1).astype(int))
 df[pre+fld] = res

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

columns = [“Date”, “Store”, “Promo”, “StateHoliday”, “SchoolHoliday”]
df = train[columns]
fld = ‘SchoolHoliday’
df = df.sort_values([‘Store’, ‘Date’])
get_elapsed(fld, ‘After’)
df = df.sort_values([‘Store’, ‘Date’], ascending=[True, False])
get_elapsed(fld, ‘Before’)
fld = ‘StateHoliday’
df = df.sort_values([‘Store’, ‘Date’])
get_elapsed(fld, ‘After’)
df = df.sort_values([‘Store’, ‘Date’], ascending=[True, False])
get_elapsed(fld, ‘Before’)
fld = ‘Promo’
df = df.sort_values([‘Store’, ‘Date’])
get_elapsed(fld, ‘After’)
df = df.sort_values([‘Store’, ‘Date’], ascending=[True, False])
get_elapsed(fld, ‘Before’)

В следующих строках кода выбраны «столбцы», для которых мы будем измерять продолжительность с помощью даты и времени. Блок кода внутри цикла double for создает дополнительные столбцы с именем, указанным путем добавления «До» или «После» к выбранным столбцам.

Две панды DataFrames созданы для «bwd» и «fwd», для перемещения назад во времени и вперед во времени. DataFrame «df» сгруппирован по имени хранилища, и каждое окно из 7 выборок суммируется как в обратном, так и в прямом времени в виде скользящего окна, сохраненного как новые DataFrames, «bwd» и «fwd» соответственно.

df = df.set_index(“Date”)
columns = [‘SchoolHoliday’, ‘StateHoliday’, ‘Promo’]
for o in [‘Before’, ‘After’]:
 for p in columns:
 a = o+p
 df[a] = df[a].fillna(0).astype(int)
 
 
bwd = df[[‘Store’]+columns].sort_index().groupby(“Store”).rolling(7, min_periods=1).sum()
fwd = df[[‘Store’]+columns].sort_index(ascending=False
 ).groupby(“Store”).rolling(7, min_periods=1).sum()

Фреймы данных «bwd» и «fwd» объединяются в исходный фрейм данных «df». По сути, мы создали новые столбцы, которые давали дополнительную информацию о продолжительности времени до и после некоторых важных событий. Это происходит как в прямом, так и в обратном направлении. Вся эта информация добавляется обратно в исходный DataFrame «df», чтобы обогатить пространство функций набора данных.

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

bwd.drop(‘Store’,1,inplace=True)
bwd.reset_index(inplace=True)
fwd.drop(‘Store’,1,inplace=True)
fwd.reset_index(inplace=True)
df.reset_index(inplace=True)
df = df.merge(bwd, ‘left’, [‘Date’, ‘Store’], suffixes=[‘’, ‘_bw’])
df = df.merge(fwd, ‘left’, [‘Date’, ‘Store’], suffixes=[‘’, ‘_fw’])
df.drop(columns,1,inplace=True)
df.head()

4. apply_cats

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

5. get_cv_idxs

Обратитесь к пункту 7 для подробного объяснения этой функции.

Эта функция используется для выборки 20% от общего количества выборок из dataFrame. Эти выборочные данные используются в качестве входных данных для модели DL.

6. proc_df

Обратитесь к пункту 7 для подробного объяснения этой функции.

7. ColumnarModelData.from_data_frame

8. md.get_learner

9. M.fit

Выше трех строк кода в 7,8 и 9 - фактическая реализация модели DL. Fast.ai упрощает жизнь людям, которые хотят внедрить модели DL в свои наборы данных. Это так же просто, как эти три строчки кода.

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

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

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

  1. Модели DL могут аппроксимировать гораздо более сложные шаблоны, чем традиционные модели машинного обучения.
  2. Модели DL хорошо умеют находить скрытые закономерности в данных. Они могут понять важность функции. И все это без предварительной обработки или с минимальной предварительной обработкой.
  3. С появлением вычислительных ресурсов GPU и TPU, доступных вместе с огромным объемом данных, неудивительно, что DL заменяет другие модели машинного обучения.

Компьютерное зрение - это первая область, которую взял на себя DL, и теперь DL также добился больших успехов в НЛП. Что касается структурированных данных, таких как столбчатые данные, DL является недавним участником, и большинство построенных здесь моделей основаны на традиционных методах машинного обучения, таких как случайный лес, логистическая регрессия, SVM и т. Д. Для DL в структурированных данных не существует стандартных моделей, которые можно было бы использовать. применяется к необработанным наборам данных. Вот почему в приведенной выше реализации нам пришлось проделать много предварительной обработки. Со временем появятся более совершенные модели DL, которые смогут избежать большей части предварительной обработки. Это также возможность для людей в сообществе воспользоваться этим ранним началом в этой области.

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

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

Я очень долго думал о написании статьи, по крайней мере, с тех пор, как я посетил курс Jeremy Howard Deep, fast.ai Deep Learning, часть 2, часть 2, май 2018 года. С большим колебанием, трудностями и натиском со стороны моего хорошего друга , наконец, пишу эту статью. Я знаю, что в этом может быть много ошибок, и вы можете столкнуться с трудностями, следуя статье, как я задумал. Есть много возможностей для улучшения. Я буду более чем счастлив ответить на вопросы и принять предложения по улучшению статьи.

Спасибо Джереми Ховарду за преподавание курса и за отличный источник вдохновения и мотивации.

Я черпал вдохновение, содержание и стиль из следующих источников:

  1. Https://github.com/fastai/fastai/blob/master/courses/dl1/lesson3-rossman.ipynb
  2. Http://forums.fast.ai/t/wiki-lesson-4/9402
  3. Http://forums.fast.ai/t/deeplearning-lec4notes/8146
  4. Https://medium.com/@hiromi_suenaga/deep-learning-2-part-1-lesson-4-2048a26d58aa
  5. Https://www.youtube.com/watch?v=gbceqO8PpBg&feature=youtu.be