X10 را معرفی می کنیم

%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 یک Backend با کارایی بالا برای TensorFlow به Swift اضافه می‌کند و از ردیابی تانسور و کامپایلر XLA استفاده می‌کند. این آموزش X10 را معرفی می کند و شما را در فرآیند به روز رسانی یک حلقه آموزشی برای اجرا بر روی GPU یا TPU راهنمایی می کند.

تانسورهای مشتاق در مقابل X10

محاسبات تسریع شده در Swift برای 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)

اگر این نوت بوک را روی یک نمونه مجهز به GPU اجرا می کنید، باید آن سخت افزار را در توضیحات دستگاه بالا مشاهده کنید. Runtime مشتاق از TPU ها پشتیبانی نمی کند، بنابراین اگر از یکی از آنها به عنوان شتاب دهنده استفاده می کنید، می بینید که CPU به عنوان یک هدف سخت افزاری استفاده می شود.

هنگام ایجاد یک 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)

اگر این را در نمونه‌ای با GPU فعال می‌کنید، باید آن شتاب‌دهنده را در دستگاه تانسور X10 مشاهده کنید. برخلاف اجرای مشتاقانه، اگر این را در نمونه‌ای با TPU فعال می‌کنید، اکنون باید ببینید که محاسبات از آن دستگاه استفاده می‌کنند. X10 روشی است که شما از مزایای TPU در Swift برای TensorFlow استفاده می کنید.

دستگاه های پیش فرض مشتاق و X10 سعی می کنند از اولین شتاب دهنده سیستم استفاده کنند. اگر پردازنده‌های گرافیکی متصل دارید، از اولین GPU موجود استفاده می‌کند. اگر 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 شما باز می کند.