Вещи, которые могут пойти не так, и как их диагностировать.

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

1 Цель модели машинного обучения

Вы можете увидеть финальную (рабочую) модель на GitHub. Я создаю модель для предсказания молний на 30 минут вперед и планирую представить ее Американскому метеорологическому обществу. Основная идея состоит в том, чтобы создать участки изображения размером 64x64 вокруг каждого пикселя данных GOES-16 инфракрасного излучения и Global Lightning Mapper (GLM) и пометить пиксель как has_ltg = 1, если изображение освещения действительно появляется через 30 минут в пределах участка изображения 16x16 вокруг пиксель.

Обученную таким образом модель можно использовать для прогнозирования молний на 30 минут вперед в режиме реального времени с учетом текущих данных инфракрасного излучения и GLM.

1а. Функция ввода

Я написал модель сверточной сети, широко заимствовав из цикла обучения модель ResNet, написанную для TPU, и адаптировал функцию ввода (для чтения моих данных, а не JPEG) и модель (простая сверточная сеть, а не ResNet).

Ключевой бит кода для ввода в тензор:

parsed = tf.parse_single_example(
    example_data, {
        'ref': tf.VarLenFeature(tf.float32),
        'ltg': tf.VarLenFeature(tf.float32),
        'has_ltg': tf.FixedLenFeature([], tf.int64, 1),
    })
parsed['ref'] = _sparse_to_dense(parsed['ref'], height * width)
parsed['ltg'] = _sparse_to_dense(parsed['ltg'], height * width)
label = tf.cast(tf.reshape(parsed['has_ltg'], shape=[]), dtype=tf.int32)

По сути, каждая запись TensorFlow (созданная конвейером Apache Beam) состоит из полей ref, ltg и has_ltg. Ref и ltg - это массивы переменной длины, преобразованные в плотные матрицы размером 64x64 (с использованием tf.sparse_tensor_to_dense). Метка просто 0 или 1.

Затем я беру два проанализированных изображения и складываю их вместе:

stacked = tf.concat([parsed['ref'], parsed['ltg']], axis=1)
img = tf.reshape(stacked, [height, width, n_channels])

На данный момент у меня есть тензор [?, 64, 64, 2], то есть пакет двухканальных изображений. Остальная часть входного конвейера в make_input_fn (перечисление файлов, чтение файлов с помощью параллельного чередования, предварительная обработка данных, статическая форма и предварительная выборка) была просто скопирована из кода ResNet.

1b. Запустите код

Я разработал код, запустив трейнер локально за несколько шагов с небольшим размером пакета:

gcloud ml-engine local train \
    --module-name=trainer.train_cnn --package-path=${PWD}/ltgpred/trainer \
    -- \
    --train_steps=10 --num_eval_records=512 --train_batch_size=16 \
    --job-dir=$OUTDIR --train_data_path=${DATADIR}/train* --eval_data_path=${DATADIR}/eval*

Затем я запустил его на более крупном наборе данных в Cloud ML Engine, используя большую машину с более мощным графическим процессором:

gcloud ml-engine jobs submit training $JOBNAME \
    --module-name=trainer.train_cnn --package-path=${PWD}/ltgpred/trainer --job-dir=$OUTDIR \
    --region=${REGION} --scale-tier=CUSTOM --config=largemachine.yaml \
    --python-version=3.5 --runtime-version=1.8 \
    -- \
    --train_data_path=${DATADIR}/train* --eval_data_path=${DATADIR}/eval* \
    --train_steps=5000 --train_batch_size=256 \
    --num_eval_records=128000 

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

2 Модель не учится

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

и достигли показателя точности, который упорно придерживался этого числа, начиная с эпохи 1000:

'rmse': 0.49927762, 'accuracy': 0.6623125,

2а. Обычные подозреваемые

Когда модель машинного обучения не обучается, есть несколько обычных подозреваемых. Пробовал изменить инициализацию. По умолчанию TensorFlow использует zeros_initializer [edit: Оказалось, мне не нужно было этого делать - tf.layers.conv2d наследуется от Keras’ Conv2D , который использует glorot_uniform, который такой же, как Xavier]. Как насчет использования Xavier (который использует небольшие начальные значения) и установки случайного начального числа для повторяемости?

xavier = tf.contrib.layers.xavier_initializer(seed=13)
c1 = tf.layers.conv2d(
       convout,
       filters=nfilters,
       kernel_size=ksize,
       kernel_initializer=xavier,
       strides=1,
       padding='same',
       activation=tf.nn.relu)

Как насчет изменения градиента со сложного AdamOptimizer на надежный резервный GradientDescentOptimizer?

Как насчет снижения скорости обучения с 0,01 до 1e-6?

Ничего из этого не помогло, но я впервые попробовал именно это.

2b. Печать в TensorFlow

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

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

print(img)

Это просто распечатает метаданные тензора, а не его значения. Вместо этого вам нужно оценить значение тензора во время выполнения программы:

print(img.eval(sess=...))

и даже это не сработает, потому что API-интерфейс оценщика не дает вам доступа к сеансу. Решение - использовать tf.Print:

img = tf.Print(img, [img], "image values=")

При этом узел печати вставляется между исходным узлом img и новым узлом img, чтобы значение было напечатано до того, как узел изображения снова будет использован:

2c. Печать статистики тензоров

Однако как только я это сделал, я получил только первые 3 или 4 значения (по умолчанию tf.Print не печатает весь тензор), и они в основном были нулевыми. Они равны нулю, потому что спутниковые изображения, как правило, содержат много нулей, или они равны нулю, потому что в моем входном конвейере есть ошибка? Просто распечатать изображение - не лучшая идея. Итак, я решил распечатать статистику входов:

numltg = tf.reduce_sum(labels)
ref = tf.slice(img, [0, 0, 0, 0], [-1, -1, -1, 1])
meanref = tf.reduce_mean(ref, [1, 2])
ltg = tf.slice(img, [0, 0, 0, 1], [-1, -1, -1, 1])
meanltg = tf.reduce_mean(ltg, [1, 2])
ylogits = tf.Print(ylogits, 
             [numltg, meanref, meanltg, ylogits], "...")

Функции reduce_ * в TensorFlow позволяют суммировать по оси. Следовательно, первая функция reduce_sum (labels) вычисляет сумму меток в пакете. Поскольку метка 0 или 1, эта сумма говорит мне количество примеров освещения в партии.

Я также хочу напечатать среднее значение отражательной способности и участки входного изображения молнии. Для этого я хочу сделать сумму по высоте и ширине, но сохранить каждый пример и канал отдельно - вот почему вы видите [1,2] в вызовах reduce_mean. Первый tf.slice получает первый канал, а второй срез получает мне второй канал (-1 в tf.slice сообщает TensorFlow, что нужно получить все элементы в этом измерении).

Также обратите внимание, что я вставил узел Print в расположение ylogits и теперь могу распечатать весь список тензоров. Это важно - вы должны вставить Print () в график на узле, который действительно используется. Если бы я сделал:

numltg = tf.Print(numltg, 
             [numltg, meanref, meanltg], "...")

вся ветка была бы оптимизирована, так как моя модель фактически нигде не использует numltg!

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

2г. Исправить тасование

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

dataset = dataset.shuffle(1024)

Изменив это на

dataset = dataset.shuffle(batch_size * 50)

решил проблему партии.

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

2e. ReLu зажимы и пропитки

Но вернемся к исходной проблеме. Почему застревают точность и среднеквадратичное значение? Мне здесь повезло. Мне пришлось куда-то вставить tf.Print (), поэтому я застрял в узле, который, как я знал, мне нужен, - на узле вывода моей модельной функции (ylogits). Мне также довелось распечатать ylogits, и о чудо… в то время как все входные данные были каждый раз разными значениями, ylogits начинались случайным образом, но быстро становились равными нулю.

Почему ylogits равен нулю? Внимательно посмотрев на расчет ylogits, я заметил, что написал:

ylogits = tf.layers.dense(
  convout, 1, activation=tf.nn.relu, kernel_initializer=xavier)

Ой! Установив функцию активации ReLu на выходном плотном слое, я удостоверился, что ylogits никогда не будет отрицательным. Между тем, молнии встречаются реже, чем молнии, поэтому оптимизатор доводил ylogits до минимально возможного значения. А это ноль. Поскольку ReLu насыщается ниже нуля, вещи могут там застрять.

Предпоследний уровень сети классификации подобен последнему уровню сети регрессии. Должно быть:

ylogits = tf.layers.dense(
  convout, 1, activation=None, kernel_initializer=xavier)

Глупый, глупый, ошибка. Нашел и исправил. Ух!

3. Потеря NaN

Теперь, когда я его запустил, у меня не было зависшей метрики точности. Худший. Я получил… «Потеря NaN во время тренировки». Если и есть что-то, что вызовет ужас в сердцах практикующих ОД, так это потери NaN. О, ну, если я смогу разобраться в этом, я получу об этом сообщение в блоге.

Как и в случае с моделью, которая не обучается, есть несколько обычных подозреваемых в потерях NaN. Попытка вычислить потерю кросс-энтропии самостоятельно - одна из них. Но я этого не делал. Я рассчитывал потерю как:

loss = tf.reduce_mean(
    tf.nn.sigmoid_cross_entropy_with_logits(
        logits=tf.reshape(ylogits, [-1]),
        labels=tf.cast(labels, dtype=tf.float32)))

Это должно быть численно стабильным. Другая проблема - слишком высокая скорость обучения. Я снова переключился на AdamOptimizer и попытался установить низкую скорость обучения (1e-6). Нет.

Другая проблема заключается в том, что входные данные сами могут содержать NaN. Это невозможно - одно из преимуществ использования TFRecords заключается в том, что TFRecordWriter не принимает значения NaN. Чтобы убедиться, я вернулся к своему входному конвейеру и добавил np.nan_to_num к фрагменту кода, который вставлял массивы в TFRecord:

def _array_feature(value):
  value = np.nan_to_num(value.flatten())
  return tf.train.Feature(float_list=tf.train.FloatList(value=value))

По-прежнему никуда.

3а. Более простая модель глубокого обучения

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

convout = img
for layer in range(nlayers):
  nfilters = (nfil // (layer+1))
  nfilters = 1 if nfilters < 1 else nfilters
  # convolution
  c1 = tf.layers.conv2d(
      convout,
      filters=nfilters,
      kernel_size=ksize,
      kernel_initializer=xavier,
      strides=1,
      padding='same',
      activation=tf.nn.relu)
  # maxpool
  convout = tf.layers.max_pooling2d(c1, pool_size=2, strides=2, padding='same')
  print('Shape of output of {}th layer = {} {}'.format(
      layer + 1, convout.shape, convout))

outlen = convout.shape[1] * convout.shape[2] * convout.shape[3]
p2flat = tf.reshape(convout, [-1, outlen])  # flattened
print('Shape of flattened conv layers output = {}'.format(p2flat.shape))

По-прежнему никуда. Потеря NaN осталась.

3b. Вернуться к линейному

Что, если я полностью удалю модель глубокого обучения? Помните мою статистику отладки по входному изображению? Что, если мы попытаемся обучить модель предсказывать освещение, основываясь только на этих разработанных функциях:

halfsize = params['predsize']
qtrsize = halfsize // 2
ref_smbox = tf.slice(img, [0, qtrsize, qtrsize, 0], [-1, halfsize, halfsize, 1])
ltg_smbox = tf.slice(img, [0, qtrsize, qtrsize, 1], [-1, halfsize, halfsize, 1])
ref_bigbox = tf.slice(img, [0, 0, 0, 0], [-1, -1, -1, 1])
ltg_bigbox = tf.slice(img, [0, 0, 0, 1], [-1, -1, -1, 1])
engfeat = tf.concat([
  tf.reduce_max(ref_bigbox, [1, 2]), # [?, 64, 64, 1] -> [?, 1]
  tf.reduce_max(ref_smbox, [1, 2]),
  tf.reduce_mean(ref_bigbox, [1, 2]),
  tf.reduce_mean(ref_smbox, [1, 2]),
  tf.reduce_mean(ltg_bigbox, [1, 2]),
  tf.reduce_mean(ltg_smbox, [1, 2])
], axis=1)

ylogits = tf.layers.dense(
  engfeat, 1, activation=None, kernel_initializer=xavier)

Я решил создать два набора статистики, один в блоке 64x64, а другой - в блоке 16x16, и создать модель логистической регрессии только с этими 6 входными функциями.

Нет. По-прежнему NaN. Это очень и очень странно. В линейной модели никогда не должно быть NaN-out. Математически это безумие.

Но незадолго до того, как это было NaN-ed, модель достигла точности 75%. Это очень многообещающе. Но эта штука с NaN становится очень надоедливой. Забавно то, что прямо перед тем, как «расходиться» с loss = NaN, модель вообще не расходилась, убытки уменьшались:

3c. Проверить входы в потерю

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

loss = tf.losses.sigmoid_cross_entropy(labels,
  tf.reshape(ylogits, [-1]))

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

with tf.control_dependencies([
  tf.Assert(tf.is_numeric_tensor(ylogits),[ylogits]),
  tf.assert_non_negative(labels, [labels]),
  tf.assert_less_equal(labels, 1, [labels])
]):
  loss = tf.losses.sigmoid_cross_entropy(labels,
    tf.reshape(ylogits, [-1]))

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

3d. Клип-градиенты

Утверждения не срабатывают, но программа все равно завершается с потерей NaN. На данный момент кажется очевидным, что проблема не во входных данных (потому что я сделал здесь nan_to_num) или в расчетах нашей модели (потому что утверждения не запускаются). Может ли это быть в обратном распространении, возможно, в вычислении градиента?

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

optimizer = tf.train.AdamOptimizer(learning_rate=0.001)
optimizer = tf.contrib.estimator.clip_gradients_by_norm(
  optimizer, 5)
train_op = optimizer.minimize(loss, tf.train.get_global_step())

Это тоже не помогло.

3e. L2 потеря

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

l2loss = tf.add_n(
  [tf.nn.l2_loss(v) for v in tf.trainable_variables()])
loss = loss + 0.001 * l2loss

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

3f. Адам Эпсилон

Поковыряясь еще немного, я понимаю, что документация для AdamOptimizer объясняет, что значение epsilon по умолчанию, равное 1e-8, может быть проблематичным. По сути, малые значения эпсилон вызывают нестабильность, даже если цель эпсилон - предотвратить деление на ноль. Возникает вопрос, почему это значение по умолчанию, но давайте попробуем большее значение, которое рекомендуется.

optimizer = tf.train.AdamOptimizer(
    learning_rate=params['learning_rate'], epsilon=0.1)

Это выталкивает NaN дальше, но все равно не работает.

3г. Различное оборудование

Другой причиной этого могут быть ошибки CUDA и тому подобное. Давайте попробуем потренироваться на другом оборудовании (с P100 вместо Tesla K80), чтобы увидеть, сохраняется ли проблема.

trainingInput:
  scaleTier: CUSTOM
  masterType: complex_model_m_p100

По-прежнему никуда.

4. Перепишите как Керас

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

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

4а. CNN с Батчнормом

Создать CNN с помощью batchnorm (который поможет сохранять градиенты в пределах допустимого диапазона) в Keras довольно просто. (Спасибо, Франсуа). Настолько просто, что я повсюду кладу батчорм.

img = keras.Input(shape=[height, width, 2])
cnn = keras.layers.BatchNormalization()(img)
for layer in range(nlayers):
  nfilters = nfil * (layer + 1)
  cnn = keras.layers.Conv2D(nfilters, (ksize, ksize), padding='same')(cnn)
  cnn = keras.layers.Activation('elu')(cnn)
  cnn = keras.layers.BatchNormalization()(cnn)
  cnn = keras.layers.MaxPooling2D(pool_size=(2, 2))(cnn)
  cnn = keras.layers.Dropout(dprob)(cnn)
cnn = keras.layers.Flatten()(cnn)
ltgprob = keras.layers.Dense(10, activation='sigmoid')(cnn)

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

optimizer = tf.keras.optimizers.Adam(lr=params['learning_rate'])
model.compile(optimizer=optimizer,
              loss='binary_crossentropy',
              metrics=['accuracy', 'mse'])

Отлично!

К сожалению (вы это уже знаете), у меня все еще есть NaN!

Гаах. Хорошо, вернемся к чертежной доске. Давайте сделаем все, что мы сделали в TensorFlow в Керасе.

4b. Функциональная инженерия в Керасе

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

def create_feateng_model(params):
  # input is a 2-channel image
  height = width = 2 * params[’predsize’]
  img = keras.Input(shape=[height, width, 2])

  engfeat = keras.layers.Lambda(
    lambda x: engineered_features(x, height//2))(img)

  ltgprob = keras.layers.Dense(1, activation=’sigmoid’)(engfeat)

  # create a model
  model = keras.Model(img, ltgprob)

Engineered_features - это та же функция TensorFlow, что и раньше! Ключевая идея заключается в том, что для обертывания функции TensorFlow в слое Keras вы можете использовать слой Lambda и вызывать функцию TensorFlow.

4c. Печать слоя

Но я хочу распечатать слой, чтобы убедиться, что цифры правильные. Как я могу это сделать? tf.Print () не будет работать, потому что у меня нет тензоров. У меня есть слои Keras.

Что ж, tf.Print () - это функция TensorFlow, поэтому используйте тот же трюк со слоем Lambda:

def print_layer(layer, message, first_n=3, summarize=1024):
  return keras.layers.Lambda((
    lambda x: tf.Print(x, [x],
                      message=message,
                      first_n=first_n,
                      summarize=summarize)))(layer)

который затем может быть вызван как:

engfeat = print_layer(engfeat, "engfeat=")

4г. Обрезка градиента

Обрезка градиента в Керасе? Легкий! Каждый оптимизатор поддерживает clipnorm.

optimizer = tf.keras.optimizers.Adam(lr=params['learning_rate'],
                                     clipnorm=1.)

Привет, мне нравится эта штука с Keras - она ​​дает мне красивый и простой API. Кроме того, он прекрасно взаимодействует с TensorFlow, предоставляя мне низкоуровневый контроль всякий раз, когда он мне нужен.

О верно. У меня все еще есть проблема с NaN

5. Разоблачение данных

И последнее, что я как бы сбрасывал со счетов. Проблема NaN также могла возникнуть из-за немасштабированных данных. Но мои данные по отражательной способности и молнии находятся в диапазоне [0,1]. Так что мне вообще не нужно масштабировать вещи.

Тем не менее, я не понимаю. Почему бы мне не нормализовать данные изображения (вычесть среднее значение, разделить на дисперсию) и посмотреть, поможет ли это.

Чтобы вычислить дисперсию, мне нужно пройтись по всему набору данных, поэтому это работа для Beam. Я мог бы сделать это в TensorFlow Transform, но пока позвольте мне взломать это в Beam.

5а. Перемешивание обучающих данных в Beam

Поскольку я переписываю свой конвейер, я мог бы исправить проблему перетасовки (см. Раздел 2d) раз и навсегда. Увеличение размера буфера перетасовки было хитростью. Я действительно не хочу записывать данные в том порядке, в котором я создавал патчи изображения. Давайте изменим порядок патчей изображений в Apache Beam:

# shuffle the examples so that each small batch doesn't contain
# highly correlated records
examples = (examples
    | '{}_reshuffleA'.format(step) >> beam.Map(
        lambda t: (random.randint(1, 1000), t))
    | '{}_reshuffleB'.format(step) >> beam.GroupByKey()
    | '{}_reshuffleC'.format(step) >> beam.FlatMap(lambda t: t[1]))

По сути, я назначаю случайный ключ (от 1 до 1000) каждой записи, группирую по этому случайному ключу, удаляю ключ и записываю записи. Теперь последовательные исправления изображения не будут следовать одно за другим.

5б. Вычисление дисперсии в Apache Beam

Как вычислить дисперсию в Apache Beam? Я сделал то, что делаю всегда, поискал в StackOverflow то, что можно скопировать и вставить. К сожалению, все, что я нашел, это вопрос без ответа. Ну ладно, пристегнитесь и напишите собственный Combiner:

import apache_beam as beam
import numpy as np
class MeanStddev(beam.CombineFn):
  def create_accumulator(self):
    return (0.0, 0.0, 0) # x, x^2, count
def add_input(self, sum_count, input):
    (sum, sumsq, count) = sum_count
    return sum + input, sumsq + input*input, count + 1
def merge_accumulators(self, accumulators):
    sums, sumsqs, counts = zip(*accumulators)
    return sum(sums), sum(sumsqs), sum(counts)
def extract_output(self, sum_count):
    (sum, sumsq, count) = sum_count
    if count:
      mean = sum / count
      variance = (sumsq / count) - mean*mean
      # -ve value could happen due to rounding
      stddev = np.sqrt(variance) if variance > 0 else 0
      return {
        'mean': mean,
        'variance': variance,
        'stddev': stddev,
        'count': count
      }
    else:
      return {
        'mean': float('NaN'),
        'variance': float('NaN'),
        'stddev': float('NaN'),
        'count': 0
      }
    
    
[1.3, 3.0, 4.2] | beam.CombineGlobally(MeanStddev())

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

Затем я пошел и добавил ответ в StackOverflow. Может, все, что мне нужно, это хорошая карма ...

5б. Запишите среднее значение, отклонение

Затем я могу перейти к своему коду конвейера и добавить:

if step == 'train':
  _ = (examples
    | 'get_values' >> beam.FlatMap(
        lambda x : [(f, x[f]) for f in ['ref', 'ltg']])
    | 'compute_stats' >> beam.CombinePerKey(MeanStddev())
    | 'write_stats' >> beam.io.Write(beam.io.WriteToText(
        os.path.join(options['outdir'], 'stats'), num_shards=1))
  )

По сути, я беру example [‘ref’] и example [‘ltg’] и создаю кортежи, которые группирую по ключу. Затем я могу вычислить среднее и стандартное отклонение каждого пикселя в обоих этих изображениях по всему набору данных.

После запуска конвейера я могу распечатать итоговую статистику:

gsutil cat gs://$BUCKET/lightning/preproc/stats*
('ltg', {'count': 1028242, 'variance': 0.0770683210620995, 'stddev': 0.2776118172234379, 'mean': 0.08414945119923131})
('ref', {'count': 1028242, 'variance': masked, 'stddev': 0, 'mean': masked})

В маске? Что означает @ # $ @ # $ @ # под маской? Оказывается, маскированные массивы - штука особенная. Маскированные значения не являются NaN, поэтому, если вы обработаете их с помощью Numpy, nan_to_num () ничего с ним не сделает. С другой стороны, он выглядит числовым, поэтому все мои утверждения TensorFlow не возникают. Числовые операции с замаскированным значением приводят к замаскированному значению.

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

5c. Изменить маскированные значения

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

ref = goesio.read_ir_data(ir_blob_path, griddef)
ref = np.ma.filled(ref, 0) # mask -> 0

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

('ref', {'count': 1028242, 'variance': 0.07368491739234752, 'stddev': 0.27144965903892293, 'mean': 0.3200035849321707})
('ltg', {'count': 1028242, 'variance': 0.0770683210620995, 'stddev': 0.2776118172234379, 'mean': 0.08414945119923131})

Это небольшие числа. Не должно быть причин масштабировать что-либо. Итак, давайте просто потренируемся, решив проблему маскировки.

На этот раз нет NaN. Вместо этого программа вылетает, и в журналах отображается:

Filling up shuffle buffer (this may take a while): 251889 of 1280000
The replica master 0 ran out-of-memory and exited with a non-zero status of 9(SIGKILL)

5г. Уменьшить размер буфера перемешивания

Нулевая реплика - это входной конвейер (чтение данных происходит на ЦП). Почему он хочет прочитать 1280000 записей сразу в буфер перемешивания?

Что ж, теперь, когда входные данные так хорошо перетасовываются моим Beam / Dataflow, мне даже не нужен такой большой буфер перемешивания (раньше он был 5000, см. Раздел 2d):

dataset = dataset.shuffle(batch_size * 50) # shuffle by a bit

И… через 12 минут тренировка завершается с точностью 83% (!!!). Нигде нет NaN.

Ух!

5e. Объедините модели

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

Вот как это сделать в Керасе, с добавлением некоторых пакетов и отсева, просто потому, что их так легко добавить:

cnn = keras.layers.BatchNormalization()(img)
for layer in range(nlayers):
  nfilters = nfil * (layer + 1)
  cnn = keras.layers.Conv2D(nfilters, (ksize, ksize), padding='same')(cnn)
  cnn = keras.layers.Activation('elu')(cnn)
  cnn = keras.layers.BatchNormalization()(cnn)
  cnn = keras.layers.MaxPooling2D(pool_size=(2, 2))(cnn)
cnn = keras.layers.Flatten()(cnn)
cnn = keras.layers.Dropout(dprob)(cnn)
cnn = keras.layers.Dense(10, activation='relu')(cnn)

# feature engineering part of model
engfeat = keras.layers.Lambda(
  lambda x: engineered_features(x, height//2))(img)

# concatenate the two parts
both = keras.layers.concatenate([cnn, engfeat])
ltgprob = keras.layers.Dense(1, activation='sigmoid')(both)

Теперь у меня точность 85%. Это все еще небольшой набор данных (спутниковые данные всего за 60 дней), и, вероятно, поэтому путь глубокого обучения не приносит такой большой пользы. Итак, я вернусь и буду генерировать больше данных, тренироваться дольше, тренироваться на TPU и т. Д.

Но это может подождать. А сейчас я иду праздновать.