تقديم X10

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


عرض على TensorFlow.org تشغيل في جوجل كولاب عرض المصدر على جيثب

افتراضيًا، يقوم Swift For TensorFlow بتنفيذ عمليات الموتر باستخدام الإرسال المتحمس. يسمح هذا بالتكرار السريع، ولكنه ليس الخيار الأكثر أداءً لتدريب نماذج التعلم الآلي.

تضيف مكتبة Tensor X10 واجهة خلفية عالية الأداء إلى Swift لـ TensorFlow، مع الاستفادة من تتبع الموتر ومترجم XLA . سيقدم هذا البرنامج التعليمي X10 ويرشدك خلال عملية تحديث حلقة التدريب للتشغيل على وحدات معالجة الرسومات أو وحدات TPU.

حريصة مقابل الموترات X10

يتم إجراء العمليات الحسابية المسرَّعة في Swift for TensorFlow من خلال نوع Tensor. يمكن للموترات المشاركة في مجموعة واسعة من العمليات، وهي اللبنات الأساسية لنماذج التعلم الآلي.

افتراضيًا، يستخدم 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، لذا إذا كنت تستخدم أحدها كمسرع، فسترى استخدام وحدة المعالجة المركزية كهدف للأجهزة.

عند إنشاء Tensor، يمكن تجاوز جهاز وضع الرغبة الافتراضي عن طريق تحديد بديل. هذه هي الطريقة التي تختار بها إجراء العمليات الحسابية باستخدام الواجهة الخلفية 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 الأولى بشكل افتراضي. إذا لم يتم العثور على مسرّع أو دعمه، فسيعود الجهاز الافتراضي إلى وحدة المعالجة المركزية (CPU).

بخلاف الأجهزة الافتراضية وأجهزة XLA، يمكنك توفير أجهزة محددة وأهداف خلفية في الجهاز:

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

تدريب نموذج وضع حريصة

دعنا نلقي نظرة على كيفية إعداد نموذج وتدريبه باستخدام وضع التنفيذ المتحمس الافتراضي. في هذا المثال، سنستخدم نموذج LeNet-5 البسيط من مستودع النماذج السريعة ومجموعة بيانات تصنيف الأرقام المكتوبة بخط اليد 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 for 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 عن طريق التتبع من خلال حسابات الموتر المطلوبة في التعليمات البرمجية الخاصة بك وتجميع تمثيل محسن لذلك التتبع في الوقت المناسب. في حالة حلقة التدريب، فإنك تكرر نفس العملية مرارًا وتكرارًا، وهو قسم مثالي للتتبع والتجميع وإعادة الاستخدام.

في حالة عدم وجود تعليمات برمجية تطلب بشكل صريح قيمة من Tensor (عادة ما تبرز هذه على شكل استدعاءات .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 for TensorFlow.