%install '.package(url: "https://github.com/tensorflow/swift-models", .branch("tensorflow-0.12"))' Datasets ImageClassificationModels
print("\u{001B}[2J")
Посмотреть на TensorFlow.org | Посмотреть исходный код на 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.