ขอแนะนำ 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 จะดำเนินการกับเทนเซอร์โดยใช้การสั่งงานอย่างกระตือรือร้น ซึ่งช่วยให้สามารถทำซ้ำได้อย่างรวดเร็ว แต่ไม่ใช่ตัวเลือกที่มีประสิทธิภาพมากที่สุดสำหรับการฝึกโมเดล Machine Learning

ไลบรารีเทนเซอร์ X10 เพิ่มแบ็กเอนด์ประสิทธิภาพสูงให้กับ Swift สำหรับ TensorFlow โดยใช้ประโยชน์จากการติดตามเทนเซอร์และ คอมไพเลอร์ 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 คุณควรเห็นว่าฮาร์ดแวร์ดังกล่าวแสดงอยู่ในคำอธิบายอุปกรณ์ด้านบน รันไทม์ที่กระตือรือร้นไม่รองรับ TPU ดังนั้นหากคุณใช้หนึ่งในนั้นเป็นตัวเร่งความเร็ว คุณจะเห็น CPU ถูกใช้เป็นเป้าหมายฮาร์ดแวร์

เมื่อสร้างเทนเซอร์ คุณสามารถแทนที่อุปกรณ์โหมดกระตือรือร้นเริ่มต้นได้โดยการระบุทางเลือกอื่น นี่คือวิธีที่คุณเลือกรับการคำนวณโดยใช้แบ็กเอนด์ 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 ต่ออยู่ ระบบจะใช้ 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 แบบธรรมดาจาก ที่เก็บ 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 ทำงานโดยการติดตามผ่านการคำนวณเทนเซอร์ที่จำเป็นในโค้ดของคุณ และรวบรวมการแสดงการติดตามที่ปรับให้เหมาะสมในเวลาที่เหมาะสม ในกรณีของลูปการฝึกอบรม คุณจะทำซ้ำการดำเนินการเดิมซ้ำแล้วซ้ำเล่า ซึ่งเป็นส่วนที่เหมาะสำหรับการติดตาม คอมไพล์ และนำกลับมาใช้ใหม่

ในกรณีที่ไม่มีโค้ดที่ร้องขอค่าจาก 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 สำหรับ TensorFlow ของคุณ