【翻譯】使用Swift語言來進行數據科學研究的指南

這篇文章首先發布在我的博客上:https://www.codewoody.com/posts/37730

Swift是蘋果公司研發的用來取代Objective C進行蘋果生態系統下軟件開發的語言。而且蘋果對於Swift的野心不至於一款APP專用的開發語言而已。
從Swfit語言發佈以來,蘋果公司就將Swift開源,並且在Swift版本迭代過程中積極聽取來自普通開發者的意見。蘋果致力於將Swift打造成跨平臺的
通用變成語言。我從Swfit發佈起就開始使用了,當時接觸Swift的時候就爲其所吸引,其引入的很多特性,如Type Interference, Optional,以及
簡潔的語言形式等等,都能搞大大提高生產效率,並且提高程序的可讀性。

現在我已經不怎麼做iOS的開發的,用Swift也偏少。這兩天突然看到了一篇名爲A Comprehensive Guide to Learn Swift from Scratch for Data Science的文章,便想立刻通讀一遍,也許在之後我可以多用Swift來做研究方面的內容。

Overview

  • Swift很快就成爲了最爲強大和有效的數據科學變成語言之一;
  • Swift和Python比較類似,因此你可以很容易地遷移到Swift上;
  • 這裏我們將會涉及Swift的基礎知識,並學會如何快速搭建第一個數據科學模型;

簡介

Python在數據科學的領域的火熱程度自然不用多少,各種各樣的排名和調查都將Python列爲數據科學編程語言的佼佼者。

Python本身是非常靈活的,作爲動態語言,你在使用Python不太需要遵守很多變成方面的潛規則,這帶來很大的靈活性。不過這導致隨着項目複雜度的增長,維護Python項目會變得比較困難。當然,性能也是一個重要的因素。一般腳本級別的數據科學應用,Python的性能並不突出,Python一般被用來當做膠水語言,主要的計算一般是其他語言實現的模塊來完成。不過複雜項目中Python的性能還是會成爲一個瓶頸。

不過要記住的一點是,數據科學是一個含義廣泛且不斷演化的學科。因此其使用的語言也要不斷演化。還記得R語言在數據科學中扮演老大角色的日子嗎?與Python同時興起的還有Julia語言。

沒錯,這裏我們就要來討論一下將Swift語言應用到數據科學中。

“I always hope that when I start looking at a new language, there will be some mind-opening new ideas to find, and Swift definitely doesn’t disappoint. Swift tries to be expressive, flexible, concise, safe, easy to use, and fast. Most languages compromise significantly in at least one of these areas.” – Jeremy Howard

Jeremy Howard【~Howard was the President and Chief Scientist at Kaggle】爲一個語言背書,且將這門語言應用到他的日常數據科學研究中時,你就應該暫時停止你手上的工作好好聽一聽了。

在這這篇文章中我們將學習Swift編程語言,以及如何將其應用到數據科學領域中【~原作者真囉嗦】。如果你是Python用戶,你會發現Swift和Python之間有很多的相似性。

Why Swift?

<i>“PyTorch was created to overcome the gaps in Tensorflow. FastAI was built to fill gaps in tooling for PyTorch. But now we’re hitting the limits of Python, and Swift has the potential to bridge this gap” </i>
<p style="text-align: right"><i>– Jeremy Howard</i></p>

今年來數據科學領域對於Swift的興趣日漸增長,幾乎人人都在討論這個話題。以下是你要學習Swfit語言的幾個原因:

  • Swift很快,幾乎接近C語言的水平;
  • 與此同時,Swift語言非常簡潔,可讀性很高。這和Python類似。【~個人認爲Swift的可讀性可比Python高多了】;
struct MyModel: Layer {
  var conv = Conv2D<Float>(filterShaper: (5, 5, 3, 6))
  var pool = MaxPool2D<Float>(2)
  var flatten = Flatten<Float>()
  var dense = Dense<Float>(16 * 5 * 5, 10)
  
  @differentiable
  func call(_ input: Tensor<Float>) -> Tensor<Float> {
    return dense(flatten(pool(conv(input))))
  }
}
class MyModel(nn.Model):
  def __init__(self):
    super().__init__()
    self.conv = nn.Conv2d(3, 6, kernal_size=5)
    self.pool = nn.MaxPool2d(2)
    self.flatten = Flatten()
    self.dense = nn.Linear(16 * 5 * 5, 10)
  
  def forward(self, input):
    return self.dense(self.flatten(self.pool(self.conv(input))))
  • 相比於Python,Swift是一門更高效,穩定,安全的編程的語言;
  • Swift更適合應用到移動應用場景。Swift是iOS的官方變成語言;
  • Swift對於自動微分操作支持非常好,因此非常適合數值計算【~參見上面的@differentiable】;
  • Swift背後有Google,Apple和FastAI的支持

下面這個視頻是Jeremy Howard談論Swift的優勢的視頻。

<iframe width="560" height="315" src="https://www.youtube.com/embed/drSpCwDFwnM" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

Swift Basic for Data Analysis

在我們開始將Swift應用於數據科學研究之前,我們先來學習一下Swift語言的基礎只是。

Swift生態

目前Swift的數據科學應用生態主要由兩個生態系統組成:

  1. 開源生態
  2. 蘋果生態

在開源生態系統中,我們可以在任何操作系統下載並運行swift。我們可以使用一些非常酷的Swift庫來構建機器學習應用,例如Swift for Tensorflow, SwiftAI以及SwiftPlot.

Swift也能讓我們無縫地從Python中引入成熟的數據科學庫,例如Numpy, pandas, matplotlib以及scikit-learn。所以如果你之前還在擔心從Python遷移到Swift上有任何無法逾越的障礙的話,現在你可以寬心了。

另一方面,蘋果公司的生態系統也有其優勢。蘋果公司提供了一些有用的庫,如CoreML,讓我們能夠在Python中訓練大型的模型並且直接導入到Swift中應用。另外,其中還包括了一些已經提前訓練好了的成熟模型,我們可以直接在iOS和macOS應用中使用。

還有一些其他的有意思的庫,比如Swift-CoreML-Transformers,可以讓我們在iPhone上使用業界最新的文字生成模型,例如GPT-2, BERT等。

<i>There are multiple differences between the two ecosystems. But the most important one is that in order to use the Apple ecosystem, you need to have an Apple machine to work on and you can only build for Apple devices like the iOS, macOS etc.</i>

現在你對Swift有了一個宏觀的瞭解了,下面我們來走進代碼。

準備Swift環境

在Google Colab【~Colaboratory 是一個免費的 Jupyter 筆記本環境,不需要進行任何設置就可以使用,並且完全在雲端運行】上提供了支持GPU和TPU的Swift版本,這裏我們直接使用這一服務,從而省去安裝過程。

<img src="https://imgs.codewoody.com/uploads/big/451eba98fbef8b87bf2473971390e66f.png" alt="" style="width: 60%"/>

你可以遵循下面的步驟創建一個啓用了Colab notebook。

  1. 打開一個空白的Swift notebook;
  2. 點擊"File",然後選擇"Save a copy in Drive" - 這會將Swift notebook保存到你的Google Drive裏面。
  3. 到這裏我們就可以在Colab裏面使用Swift了。我們來寫下第一行代碼:
print("hello world from Swift")

這就是Swift的hello world程序了!接下來如果你想在本地運行Swift,你可以按照如下的鏈接進行操作:

  1. Swift安裝指南:install instructions
  2. 要在Ubuntu中安裝Jupyter Notebook:[Jeremy Howard's instructions to install Swift];
  3. 在Ubuntu上也可以使用Docker來安裝Swift:Swift for Docker

如果在macOS下面,直接從應用商店安裝xcode就行,可以創建一個Swift Playground來試試Swift語言的特性。我記得iPad上也有Swift Playground的應用。

接下來讓我們快速過一下Swift的基本語言特性。

The Print function

hello world程序中,print函數的形式一點都不陌生啦。

print("Swift is easy to learn!")

Variable in Swift

Swift提供了兩個創建變量的選項:letvar。其中let被用來創建常量,常量的值在其聲明週期中是不能被改變的。var用來創建變量,這意味着類似在Python裏一樣,你可以修改變量的值。

我們來看下面的例子。創建兩個變量:

let a = "Analytics"
var b = "Vidhya"

讓我們來嘗試修改其值:

b = "AV"
a = "AV"

我們可以看到修改a的值時會出現錯誤:

這種支持創建常量的能力可以幫助我們消除很多潛在bug。後面你可以看到我們會用let來創建那些非常重要且我們不希望修改的值。例如訓練數據和結果我們會用let來創建,而一些臨時變量會使用var來創建。

Swift的另一個很酷的特性是你可以使用emoji來作爲變量名【~其實就是對Unicode的支持】

我們也可以使用希臘字母來作爲變量名稱:

var π = 3.1415925

Swift數據類型

Swift支持一些通用的類型,如整型,字符串,單精度浮點數(Float)和雙精度浮點數(Double)。在創建變量時,Swift會根據初始化值自動推斷變量的類型。

let marks = 63
let percentage = 70.0
let name = "Sushil"

在創建變量時你也可以顯式的聲明變量類型。如果初始化值和聲明的類型不同,Swift會拋出錯誤。

let weight: Double = 62.8

字符串格式化的方式在Swift中非常簡潔。只需要用反斜槓\後面跟上括號就可以了:

let no_of_apples = 3
print("I have \(no_of_apples) apples")

你可以使用連續的三個雙引號"""來創建多行字符串。

列表和字典(List and Dictionaries)

如同Python裏面一樣,Swift裏面也支持List和Dictionary數據結構。不同於Python,在Swift中這兩種類型都使用方括號[]

var shoppingList = ["catfish", "water", "tulip", "blue paint"]
shoppingList[1] = "bottle of water"

var occupationsDist = [
  "Malcolm": "Caption",
  "Kaylee": "Mechanic"
]

ccupationsDict["Jayne"] = "Public Relations"

循環

除了支持經典的循環之外,Swift有一些自定義的比較獨特的循環形式:

for...in loop

類似Python的寫法,在Swift中,你可以以如下形式來遍歷列表Lists或者ranges

for i in 0...5 {
  print(i)
}

var someList = [20, 30, 10, 40]
for item in someList {
  print(item * 2)
}

上面的連續三個點的符號用來創建ranges。...創建的兩側是閉集, 如果要創建不包含最右側的變量的範圍,使用..<符號即可。

注意Swift使用花括號,而非縮進形式來表示代碼層次結構

在Swift中也可以使用比較經典的while和for循環。You can learn more about loops in Swift here

條件

這裏就是非常經典的if語句了,不做贅述。

Swift中條件語句針對Optional類型做了專門的優化。

函數

下圖是Swift函數的定義形式

代碼中的註釋

Swift中的註釋形式和C/C++比較像:用//來開始行註釋,用/* ... */來常見塊註釋。在代碼中多寫註釋是一個好習慣。

在Swift中使用Python的庫

Swift支持和Python的互操作,這意味着你可以直接在Swift中使用大部分Python庫:調用函數或者做變量的類型轉換。這個特性大大增強了Swift的功能。儘管Swift的生態還非常年輕,但是我們可以直接使用非常成熟的Python庫,如Numpy,Pandas還有Matplotlib等。

爲了引用Python模塊,我們只需要將Swift的Python模塊導入,然後使用這個模塊的接口即可:

import Python

// Load numpy from python
let np = Python.import("numpy")

// create array of zeros
var zeros = np.ones([2, 3])
print(zeros)

matplotlib庫也可以直接導入:

在Swift中使用Tensorflow創建一個基礎模型

Swift4Tensorflow是Swift生態中一個非常成熟的庫。我們可以用非常類似Keras的方式來創建機器學習和深度學習的模塊。

有意思的是,Swift4Tensorflow不只是一個簡單的Tensorflow的Swift語言打包,而是根據Swift本身語言開發的庫。未來這個庫可能會變成Swift的語言的核心部分。

<i>What this means is that the amazing set of Engineers from Apple’s Swift team and Google’s Tensorflow team will make sure that you are able to do high-performance machine learning in Swift.</i>

這個庫加入了一些Swift語言的有用特性,如自動微分支持(這讓我想起了PyTorch中的Autogrid)。

關於數據集

首先讓我們來解釋一下這個section的問題。如果你之前接觸過深度學習領域,你應該比較熟悉了。

我們將會建立一個卷積神經網絡(CNN)模型來將MNIST數據集中的圖片識別爲數字字符。MNIST數據集包括60,000個訓練圖像和10,000個測試圖像。圖像爲手寫的數字字符。

這個數據集是研究計算機視覺的時候一個非常常用的數據集,所以我在這裏不做細節性的描述。要了解更多,你可以讀一下這個

配置羨慕

在我們開始創建模塊之前。我們需要下載數據集並進行預處理。爲了你的方便我已經創建了一個Github倉庫,提供了預處理代碼以及數據。讓我們下載配置代碼,下載數據集並導入黑色的庫。

%include "EnableIPythonDisplay.swift"
IPythonDisplay.shell.enable_matplotlib("inline")

import Foundation
import Python

let os = Python.import("os")
let plt = Python.import("matplotlib.pyplot")

os.system("git clone https://github.com/mohdsanadzakirizvi/swift-datascience.git")
os.chdir("/content/swift-datascience")

運行上面的代碼,數據集就會下載到Colab的環境中了。

在本地運行時代碼應該需要修改,這個我們後面來討論

不過這個操作太醜陋了,沒有使用Swift的native方法來調用shell命令。

載入數據

%include "/content/swift-datascience/MNIST.swift"

// Load dataset
let dataset = MNIST(batchSize: 128)

// Get first 5 images
let imgs = dataset.trainingImages.minibatch(at: 0, batchSize: 5).makeNumpyArray()
print(imgs.shape)

查看一下數據集

我們嘗試畫出數據集中的圖片來看看我們要處理的問題:

# Display first 5 images
for img in imgs{
  plt.imshow(img.reshape(28,28))
  plt.show()
}

畫出來大概是下面的樣子:

定義模型結構

現在讓我們來定義我們的模型的結構。這裏我使用了LeNet-5架構,一個非常基礎的CNN模型,包含兩個卷基層,average pooling還有三個Dense層【~應該是全連接層?】。最後一級dense layer的輸出維數是10,因爲我們有10個類別要輸出,分別代表0-9.

import TensorFlow

let epochCount = 100
let batchSize = 128

// The LeNet-5 model
var classifier = Sequential {
    Conv2D<Float>(filterShape: (5, 5, 1, 6), padding: .same, activation: relu)
    AvgPool2D<Float>(poolSize: (2, 2), strides: (2, 2))
    Conv2D<Float>(filterShape: (5, 5, 6, 16), activation: relu)
    AvgPool2D<Float>(poolSize: (2, 2), strides: (2, 2))
    Flatten<Float>()
    Dense<Float>(inputSize: 400, outputSize: 120, activation: relu)
    Dense<Float>(inputSize: 120, outputSize: 84, activation: relu)
    Dense<Float>(inputSize: 84, outputSize: 10, activation: softmax)
}

你可能已經注意到了,上面的代碼和你在Keras(或者PyTorch,TensorFlow)中寫的Python代碼非常類似

<i>The simplicity of writing code is one of the biggest points of Swift.</i>

Swift4Tensorflow支持很多現成的多層模型。更多閱讀參考:https://www.tensorflow.org/swift/api_docs/Structs

選擇梯度下降作爲Optimizer

類似的,這裏我們也需要選擇Optimizer來優化我們的模型。我們這裏選擇使用隨機梯度下降算法(stochastic gradient descent, SGD)。

let optimizer = SGD(for: classifier, learningRate: 0.1)

Swift4Tensorflow還支持很多Optimizer:

  • AMSGrad
  • AdaDelta
  • AdaGrad
  • AdaMax
  • Adam
  • Parameter
  • RMSProp
  • SGD

模型訓練

現在萬事俱備了,讓我們開始訓練模型吧。

print("Beginning training...")

struct Statistics {
    var correctGuessCount: Int = 0
    var totalGuessCount: Int = 0
    var totalLoss: Float = 0
}

// Store accuracy results during training
var trainAccuracyResults: [Float] = []
var testAccuracyResults: [Float] = []

// The training loop.
for epoch in 1...epochCount {
    var trainStats = Statistics()
    var testStats = Statistics()

    // Set context to training 
    Context.local.learningPhase = .training

    for i in 0 ..< dataset.trainingSize / batchSize {
        // Get mini-batches of x and y
        let x = dataset.trainingImages.minibatch(at: i, batchSize: batchSize)
        let y = dataset.trainingLabels.minibatch(at: i, batchSize: batchSize)
        
        // Compute the gradient with respect to the model.
        let 𝛁model = classifier.gradient { classifier -> Tensor<Float> in
            let ŷ = classifier(x)
            let correctPredictions = ŷ.argmax(squeezingAxis: 1) .== y
            
            trainStats.correctGuessCount += Int(Tensor<Int32>(correctPredictions).sum().scalarized())
            trainStats.totalGuessCount += batchSize
            
            let loss = softmaxCrossEntropy(logits: ŷ, labels: y)
            trainStats.totalLoss += loss.scalarized()
            
            return loss
        }
        
        // Update the model's differentiable variables along the gradient vector.
        optimizer.update(&classifier, along: 𝛁model)
    }

    // Set context to inference
    Context.local.learningPhase = .inference

    for i in 0 ..< dataset.testSize / batchSize {
        let x = dataset.testImages.minibatch(at: i, batchSize: batchSize)
        let y = dataset.testLabels.minibatch(at: i, batchSize: batchSize)

        // Compute loss on test set
        let ŷ = classifier(x)
        let correctPredictions = ŷ.argmax(squeezingAxis: 1) .== y

        testStats.correctGuessCount += Int(Tensor<Int32>(correctPredictions).sum().scalarized())
        testStats.totalGuessCount += batchSize

        let loss = softmaxCrossEntropy(logits: ŷ, labels: y)

        testStats.totalLoss += loss.scalarized()
    }

    let trainAccuracy = Float(trainStats.correctGuessCount) / Float(trainStats.totalGuessCount)
    let testAccuracy = Float(testStats.correctGuessCount) / Float(testStats.totalGuessCount)

    // Save train and test accuracy
    trainAccuracyResults.append(trainAccuracy)
    testAccuracyResults.append(testAccuracy)

    print("""
          [Epoch \(epoch)] \
          Training Loss: \(trainStats.totalLoss), \
          Training Accuracy: \(trainStats.correctGuessCount)/\(trainStats.totalGuessCount) \
          (\(trainAccuracy)), \
          Test Loss: \(testStats.totalLoss), \
          Test Accuracy: \(testStats.correctGuessCount)/\(testStats.totalGuessCount) \
          (\(testAccuracy))
          """)
}

上面的代碼中用了一些fancy的數學符號,但是由於這些符號輸入並不方便,因此實際編程中我們不會這麼做。

上面的代碼流程中我們將數據集的樣本傳遞給模型,幫助其改善預測精度。訓練步驟如下:

  1. 訓練重複若干次,每次我們遍歷整個訓練集。
  2. 在每次訓練迭代中,我們逐個傳入features(x)和labels(y),這對下一步非常重要。
  3. 使用樣本的features,使用模型做出預測,並與labels提供的真值進行比對,進而計算出模型的損失函數和下降梯度方向。
  4. 這是梯度下降算法發揮了作用,我們沿着梯度方向更新模型的變量。
  5. 追蹤訓練過程中的一些統計數據來方便我們後續做可視化。
  6. 在第一步提到的重複訓練中,每次重複2至5步。

epochCount變量爲重複遍歷數據集的次數。你可以修改其值嘗試一下。

需要多少次遍歷來取得90%以上的正確率呢?我可以在12次訓練下在訓練集和測試集上獲得97%以上的正確率。

<img src="https://imgs.codewoody.com/uploads/big/5b367d407d32802ecce0bcd33b3e9f9a.png" alt="" style="border: none;"/>

可視化輸出訓練過程

用下面的方法我麼可以可視化輸出訓練過程中的誤差演變過程:

plt.figure(figsize: [12, 8])

let accuracyAxes = plt.subplot(2, 1, 1)
accuracyAxes.set_ylabel("Train Accuracy")
accuracyAxes.plot(trainAccuracyResults, color: "blue")

let lossAxes = plt.subplot(2, 1, 2)
lossAxes.set_ylabel("Test Accuracy")
lossAxes.set_xlabel("Epoch")
lossAxes.plot(testAccuracyResults, color: "yellow")

plt.show()

得到的結果如下圖所示:

Swift數據科學應用的未來

有產業專家對Swift做出了很高的評價,認爲其有潛力成爲數據科學的主流語言,同時也能成爲機器學習類應用開發的主要工具。

目前,很多fancy的數據科學相關的Swift庫還在開發中,其背後有強大的業界支持。我非常看好Swift生態的未來--甚至會比現在的Python更加強大。

下面是一些你可以進一步研究的Swift庫:

本文涉及的所有代碼託管在Github上

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章