Представляем Х10

%install '.package(url: "https://github.com/tensorflow/swift-models", .branch("tensorflow-0.12"))' Datasets ImageClassificationModels
print("\u{001B}[2J")


Посмотреть на TensorFlow.org Запустить в Google Colab Посмотреть исходный код на GitHub

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

Тензорная библиотека X10 добавляет в Swift высокопроизводительный бэкенд для TensorFlow, использующий тензорную трассировку и компилятор XLA . В этом учебном пособии вы познакомитесь с X10 и проведете вас через процесс обновления цикла обучения для работы на графических процессорах или TPU.

Тензоры Eager и X10

Ускоренные вычисления в Swift для TensorFlow выполняются через тип Tensor. Тензоры могут участвовать в самых разных операциях и являются фундаментальными строительными блоками моделей машинного обучения.

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

import TensorFlow
import Foundation
let eagerTensor1 = Tensor([0.0, 1.0, 2.0])
let eagerTensor2 = Tensor([1.5, 2.5, 3.5])
let eagerTensorSum = eagerTensor1 + eagerTensor2
print(eagerTensorSum)
[1.5, 3.5, 5.5]

print(eagerTensor1.device)
Device(kind: .CPU, ordinal: 0, backend: .TF_EAGER)

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

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

let x10Tensor1 = Tensor([0.0, 1.0, 2.0], on: Device.defaultXLA)
let x10Tensor2 = Tensor([1.5, 2.5, 3.5], on: Device.defaultXLA)
let x10TensorSum = x10Tensor1 + x10Tensor2
print(x10TensorSum)
[1.5, 3.5, 5.5]

print(x10Tensor1.device)
Device(kind: .CPU, ordinal: 0, backend: .XLA)

Если вы запускаете это в экземпляре с поддержкой графического процессора, вы должны увидеть этот ускоритель в списке тензорного устройства X10. В отличие от нетерпеливого выполнения, если вы запускаете его в экземпляре с поддержкой TPU, вы теперь должны видеть, что вычисления используют это устройство. X10 — это то, как вы можете использовать преимущества TPU в Swift для TensorFlow.

Устройства по умолчанию и устройства X10 попытаются использовать первый ускоритель в системе. Если у вас подключены графические процессоры, будет использоваться первый доступный графический процессор. Если TPU присутствуют, X10 по умолчанию будет использовать первое ядро ​​TPU. Если ускоритель не найден или не поддерживается, в качестве устройства по умолчанию будет использоваться ЦП.

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

// let tpu1 = Device(kind: .TPU, ordinal: 1, backend: .XLA)
// let tpuTensor1 = Tensor([0.0, 1.0, 2.0], on: tpu1)

Обучение модели нетерпеливого режима

Давайте посмотрим, как настроить и обучить модель, используя режим активного выполнения по умолчанию. В этом примере мы будем использовать простую модель LeNet-5 из репозитория Swift-Models и набор данных классификации рукописных цифр MNIST.

Сначала мы настроим и загрузим набор данных MNIST.

import Datasets

let epochCount = 5
let batchSize = 128
let dataset = MNIST(batchSize: batchSize)
Loading resource: train-images-idx3-ubyte
File does not exist locally at expected path: /home/kbuilder/.cache/swift-models/datasets/MNIST/train-images-idx3-ubyte and must be fetched
Fetching URL: https://storage.googleapis.com/cvdf-datasets/mnist/train-images-idx3-ubyte.gz...
Archive saved to: /home/kbuilder/.cache/swift-models/datasets/MNIST
Loading resource: train-labels-idx1-ubyte
File does not exist locally at expected path: /home/kbuilder/.cache/swift-models/datasets/MNIST/train-labels-idx1-ubyte and must be fetched
Fetching URL: https://storage.googleapis.com/cvdf-datasets/mnist/train-labels-idx1-ubyte.gz...
Archive saved to: /home/kbuilder/.cache/swift-models/datasets/MNIST
Loading resource: t10k-images-idx3-ubyte
File does not exist locally at expected path: /home/kbuilder/.cache/swift-models/datasets/MNIST/t10k-images-idx3-ubyte and must be fetched
Fetching URL: https://storage.googleapis.com/cvdf-datasets/mnist/t10k-images-idx3-ubyte.gz...
Archive saved to: /home/kbuilder/.cache/swift-models/datasets/MNIST
Loading resource: t10k-labels-idx1-ubyte
File does not exist locally at expected path: /home/kbuilder/.cache/swift-models/datasets/MNIST/t10k-labels-idx1-ubyte and must be fetched
Fetching URL: https://storage.googleapis.com/cvdf-datasets/mnist/t10k-labels-idx1-ubyte.gz...
Archive saved to: /home/kbuilder/.cache/swift-models/datasets/MNIST

Далее мы настроим модель и оптимизатор.

import ImageClassificationModels

var eagerModel = LeNet()
var eagerOptimizer = SGD(for: eagerModel, learningRate: 0.1)

Теперь мы внедрим базовое отслеживание прогресса и отчетность. Вся промежуточная статистика хранится в виде тензоров на том же устройстве, где выполняется обучение, а scalarized() вызывается только во время создания отчетов. Это будет особенно важно позже при использовании X10, поскольку позволяет избежать ненужной материализации ленивых тензоров.

struct Statistics {
    var correctGuessCount = Tensor<Int32>(0, on: Device.default)
    var totalGuessCount = Tensor<Int32>(0, on: Device.default)
    var totalLoss = Tensor<Float>(0, on: Device.default)
    var batches: Int = 0
    var accuracy: Float { 
        Float(correctGuessCount.scalarized()) / Float(totalGuessCount.scalarized()) * 100 
    } 
    var averageLoss: Float { totalLoss.scalarized() / Float(batches) }

    init(on device: Device = Device.default) {
        correctGuessCount = Tensor<Int32>(0, on: device)
        totalGuessCount = Tensor<Int32>(0, on: device)
        totalLoss = Tensor<Float>(0, on: device)
    }

    mutating func update(logits: Tensor<Float>, labels: Tensor<Int32>, loss: Tensor<Float>) {
        let correct = logits.argmax(squeezingAxis: 1) .== labels
        correctGuessCount += Tensor<Int32>(correct).sum()
        totalGuessCount += Int32(labels.shape[0])
        totalLoss += loss
        batches += 1
    }
}

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

print("Beginning training...")

for (epoch, batches) in dataset.training.prefix(epochCount).enumerated() {
    let start = Date()
    var trainStats = Statistics()
    var testStats = Statistics()

    Context.local.learningPhase = .training
    for batch in batches {
        let (images, labels) = (batch.data, batch.label)
        let 𝛁model = TensorFlow.gradient(at: eagerModel) { eagerModel -> Tensor<Float> in
            let ŷ = eagerModel(images)
            let loss = softmaxCrossEntropy(logits: ŷ, labels: labels)
            trainStats.update(logits: ŷ, labels: labels, loss: loss)
            return loss
        }
        eagerOptimizer.update(&eagerModel, along: 𝛁model)
    }

    Context.local.learningPhase = .inference
    for batch in dataset.validation {
        let (images, labels) = (batch.data, batch.label)
        let ŷ = eagerModel(images)
        let loss = softmaxCrossEntropy(logits: ŷ, labels: labels)
        testStats.update(logits: ŷ, labels: labels, loss: loss)
    }

    print(
        """
        [Epoch \(epoch)] \
        Training Loss: \(String(format: "%.3f", trainStats.averageLoss)), \
        Training Accuracy: \(trainStats.correctGuessCount)/\(trainStats.totalGuessCount) \
        (\(String(format: "%.1f", trainStats.accuracy))%), \
        Test Loss: \(String(format: "%.3f", testStats.averageLoss)), \
        Test Accuracy: \(testStats.correctGuessCount)/\(testStats.totalGuessCount) \
        (\(String(format: "%.1f", testStats.accuracy))%) \
        seconds per epoch: \(String(format: "%.1f", Date().timeIntervalSince(start)))
        """)
}
Beginning training...
[Epoch 0] Training Loss: 0.528, Training Accuracy: 50154/59904 (83.7%), Test Loss: 0.168, Test Accuracy: 9468/10000 (94.7%) seconds per epoch: 11.9
[Epoch 1] Training Loss: 0.133, Training Accuracy: 57488/59904 (96.0%), Test Loss: 0.107, Test Accuracy: 9659/10000 (96.6%) seconds per epoch: 11.7
[Epoch 2] Training Loss: 0.092, Training Accuracy: 58193/59904 (97.1%), Test Loss: 0.069, Test Accuracy: 9782/10000 (97.8%) seconds per epoch: 11.8
[Epoch 3] Training Loss: 0.071, Training Accuracy: 58577/59904 (97.8%), Test Loss: 0.066, Test Accuracy: 9794/10000 (97.9%) seconds per epoch: 11.8
[Epoch 4] Training Loss: 0.059, Training Accuracy: 58800/59904 (98.2%), Test Loss: 0.064, Test Accuracy: 9800/10000 (98.0%) seconds per epoch: 11.8

Как видите, модель обучалась так, как мы и ожидали, и ее точность относительно проверочного набора увеличивалась с каждой эпохой. Вот как модели Swift для TensorFlow определяются и запускаются с использованием быстрого выполнения. Теперь давайте посмотрим, какие изменения необходимо внести, чтобы воспользоваться преимуществами X10.

Обучение модели X10

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

let device = Device.defaultXLA
print(device)
Device(kind: .CPU, ordinal: 0, backend: .XLA)

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

В случае модели и оптимизатора мы инициализируем их с помощью своих внутренних тензоров на активном исполнительном устройстве, а затем перемещаем их на устройство X10.

var x10Model = LeNet()
x10Model.move(to: device)

var x10Optimizer = SGD(for: x10Model, learningRate: 0.1)
x10Optimizer = SGD(copying: x10Optimizer, to: device)

Модификации, необходимые для цикла обучения, происходят в нескольких конкретных моментах. Во-первых, нам нужно переместить пакеты обучающих данных на устройство X10. Это делается с помощью Tensor(copying:to:) при извлечении каждого пакета.

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

При отсутствии кода, который явно запрашивает значение у тензора (обычно они выделяются как вызовы .scalars или .scalarized() ), X10 попытается скомпилировать все итерации цикла вместе. Чтобы предотвратить это и обрезать трассировку в определенной точке, мы помещаем явный LazyTensorBarrier() после того, как оптимизатор обновит веса модели и после того, как потери и точность будут получены во время проверки. При этом создаются две повторно используемые трассировки: каждый шаг в цикле обучения и каждый пакет выводов во время проверки.

Эти изменения приводят к следующему циклу обучения.

print("Beginning training...")

for (epoch, batches) in dataset.training.prefix(epochCount).enumerated() {
    let start = Date()
    var trainStats = Statistics(on: device)
    var testStats = Statistics(on: device)

    Context.local.learningPhase = .training
    for batch in batches {
        let (eagerImages, eagerLabels) = (batch.data, batch.label)
        let images = Tensor(copying: eagerImages, to: device)
        let labels = Tensor(copying: eagerLabels, to: device)
        let 𝛁model = TensorFlow.gradient(at: x10Model) { x10Model -> Tensor<Float> in
            let ŷ = x10Model(images)
            let loss = softmaxCrossEntropy(logits: ŷ, labels: labels)
            trainStats.update(logits: ŷ, labels: labels, loss: loss)
            return loss
        }
        x10Optimizer.update(&x10Model, along: 𝛁model)
        LazyTensorBarrier()
    }

    Context.local.learningPhase = .inference
    for batch in dataset.validation {
        let (eagerImages, eagerLabels) = (batch.data, batch.label)
        let images = Tensor(copying: eagerImages, to: device)
        let labels = Tensor(copying: eagerLabels, to: device)
        let ŷ = x10Model(images)
        let loss = softmaxCrossEntropy(logits: ŷ, labels: labels)
        LazyTensorBarrier()
        testStats.update(logits: ŷ, labels: labels, loss: loss)
    }

    print(
        """
        [Epoch \(epoch)] \
        Training Loss: \(String(format: "%.3f", trainStats.averageLoss)), \
        Training Accuracy: \(trainStats.correctGuessCount)/\(trainStats.totalGuessCount) \
        (\(String(format: "%.1f", trainStats.accuracy))%), \
        Test Loss: \(String(format: "%.3f", testStats.averageLoss)), \
        Test Accuracy: \(testStats.correctGuessCount)/\(testStats.totalGuessCount) \
        (\(String(format: "%.1f", testStats.accuracy))%) \
        seconds per epoch: \(String(format: "%.1f", Date().timeIntervalSince(start)))
        """)
}
Beginning training...
[Epoch 0] Training Loss: 0.421, Training Accuracy: 51888/59904 (86.6%), Test Loss: 0.134, Test Accuracy: 9557/10000 (95.6%) seconds per epoch: 18.6
[Epoch 1] Training Loss: 0.117, Training Accuracy: 57733/59904 (96.4%), Test Loss: 0.085, Test Accuracy: 9735/10000 (97.3%) seconds per epoch: 14.9
[Epoch 2] Training Loss: 0.080, Training Accuracy: 58400/59904 (97.5%), Test Loss: 0.068, Test Accuracy: 9791/10000 (97.9%) seconds per epoch: 13.1
[Epoch 3] Training Loss: 0.064, Training Accuracy: 58684/59904 (98.0%), Test Loss: 0.056, Test Accuracy: 9804/10000 (98.0%) seconds per epoch: 13.5
[Epoch 4] Training Loss: 0.053, Training Accuracy: 58909/59904 (98.3%), Test Loss: 0.063, Test Accuracy: 9779/10000 (97.8%) seconds per epoch: 13.4

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

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

Как уже говорилось ранее, использование X10 теперь делает не только возможным, но и простым работу с TPU, открывая целый класс ускорителей для ваших моделей Swift для TensorFlow.