【翻译】使用Swift语言来进行数据科学研究的指南

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来做研究方面的内容。

1 Overview

  • Swift很快就成为了最为强大和有效的数据科学变成语言之一;
  • Swift和Python比较类似,因此你可以很容易地迁移到Swift上;
  • 这里我们将会涉及Swift的基础知识,并学会如何快速搭建第一个数据科学模型;

2 简介

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之间有很多的相似性。

3 Why Swift?

“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”

– Jeremy Howard

今年来数据科学领域对于Swift的兴趣日渐增长,几乎人人都在讨论这个话题。以下是你要学习Swfit语言的几个原因:

  • Swift很快,几乎接近C语言的水平;
  • 与此同时,Swift语言非常简洁,可读性很高。这和Python类似。【~个人认为Swift的可读性可比Python高多了】;
1
2
3
4
5
6
7
8
9
10
11
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))))
}
}
1
2
3
4
5
6
7
8
9
10
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的优势的视频。

4 Swift Basic for Data Analysis

在我们开始将Swift应用于数据科学研究之前,我们先来学习一下Swift语言的基础只是。

4.1 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等。

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.

现在你对Swift有了一个宏观的了解了,下面我们来走进代码。

4.2 准备Swift环境

在Google Colab【~Colaboratory 是一个免费的 Jupyter 笔记本环境,不需要进行任何设置就可以使用,并且完全在云端运行】上提供了支持GPU和TPU的Swift版本,这里我们直接使用这一服务,从而省去安装过程。

你可以遵循下面的步骤创建一个启用了Colab notebook。

  1. 打开一个空白的Swift notebook;
  2. 点击"File",然后选择"Save a copy in Drive" - 这会将Swift notebook保存到你的Google Drive里面。
  3. 到这里我们就可以在Colab里面使用Swift了。我们来写下第一行代码:
1
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的基本语言特性。

4.3 The Print function

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

1
print("Swift is easy to learn!")

4.4 Variable in Swift

Swift提供了两个创建变量的选项:letvar。其中let被用来创建常量,常量的值在其声明周期中是不能被改变的。var用来创建变量,这意味着类似在Python里一样,你可以修改变量的值。

我们来看下面的例子。创建两个变量:

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

让我们来尝试修改其值:

1
2
b = "AV"
a = "AV"

我们可以看到修改a的值时会出现错误:

Colab上的截图

这种支持创建常量的能力可以帮助我们消除很多潜在bug。后面你可以看到我们会用let来创建那些非常重要且我们不希望修改的值。例如训练数据和结果我们会用let来创建,而一些临时变量会使用var来创建。

Swift的另一个很酷的特性是你可以使用emoji来作为变量名【~其实就是对Unicode的支持】

我们也可以使用希腊字母来作为变量名称:

1
var π = 3.1415925

4.5 Swift数据类型

Swift支持一些通用的类型,如整型,字符串,单精度浮点数(Float)和双精度浮点数(Double)。在创建变量时,Swift会根据初始化值自动推断变量的类型。

1
2
3
let marks = 63
let percentage = 70.0
let name = "Sushil"

在创建变量时你也可以显式的声明变量类型。如果初始化值和声明的类型不同,Swift会抛出错误。

1
let weight: Double = 62.8

字符串格式化的方式在Swift中非常简洁。只需要用反斜杠\后面跟上括号就可以了:

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

你可以使用连续的三个双引号"""来创建多行字符串。

4.6 列表和字典(List and Dictionaries)

如同Python里面一样,Swift里面也支持List和Dictionary数据结构。不同于Python,在Swift中这两种类型都使用方括号[]

1
2
3
4
5
6
7
8
9
var shoppingList = ["catfish", "water", "tulip", "blue paint"]
shoppingList[1] = "bottle of water"

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

ccupationsDict["Jayne"] = "Public Relations"

4.7 循环

除了支持经典的循环之外,Swift有一些自定义的比较独特的循环形式:

4.7.1 for...in loop

类似Python的写法,在Swift中,你可以以如下形式来遍历列表Lists或者ranges

1
2
3
4
5
6
7
8
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

4.8 条件

这里就是非常经典的if语句了,不做赘述。

Swift中条件语句针对Optional类型做了专门的优化。

4.9 函数

下图是Swift函数的定义形式

4.10 代码中的注释

Swift中的注释形式和C/C++比较像:用//来开始行注释,用/* ... */来常见块注释。在代码中多写注释是一个好习惯。

4.11 在Swift中使用Python的库

Swift支持和Python的互操作,这意味着你可以直接在Swift中使用大部分Python库:调用函数或者做变量的类型转换。这个特性大大增强了Swift的功能。尽管Swift的生态还非常年轻,但是我们可以直接使用非常成熟的Python库,如Numpy,Pandas还有Matplotlib等。

为了引用Python模块,我们只需要将Swift的Python模块导入,然后使用这个模块的接口即可:

1
2
3
4
5
6
7
8
import Python

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

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

matplotlib库也可以直接导入:

5 在Swift中使用Tensorflow创建一个基础模型

Swift4Tensorflow是Swift生态中一个非常成熟的库。我们可以用非常类似Keras的方式来创建机器学习和深度学习的模块。

有意思的是,Swift4Tensorflow不只是一个简单的Tensorflow的Swift语言打包,而是根据Swift本身语言开发的库。未来这个库可能会变成Swift的语言的核心部分。

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.

这个库加入了一些Swift语言的有用特性,如自动微分支持(这让我想起了PyTorch中的Autogrid)。

5.1 关于数据集

首先让我们来解释一下这个section的问题。如果你之前接触过深度学习领域,你应该比较熟悉了。

我们将会建立一个卷积神经网络(CNN)模型来将MNIST数据集中的图片识别为数字字符。MNIST数据集包括60,000个训练图像和10,000个测试图像。图像为手写的数字字符。

这个数据集是研究计算机视觉的时候一个非常常用的数据集,所以我在这里不做细节性的描述。要了解更多,你可以读一下这个

5.2 配置羡慕

在我们开始创建模块之前。我们需要下载数据集并进行预处理。为了你的方便我已经创建了一个Github仓库,提供了预处理代码以及数据。让我们下载配置代码,下载数据集并导入黑色的库。

1
2
3
4
5
6
7
8
9
10
11
%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命令。

5.3 载入数据

1
2
3
4
5
6
7
8
%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)

5.4 查看一下数据集

我们尝试画出数据集中的图片来看看我们要处理的问题:

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

画出来大概是下面的样子:

5.5 定义模型结构

现在让我们来定义我们的模型的结构。这里我使用了LeNet-5架构,一个非常基础的CNN模型,包含两个卷基层,average pooling还有三个Dense层【~应该是全连接层?】。最后一级dense layer的输出维数是10,因为我们有10个类别要输出,分别代表0-9.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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代码非常类似

The simplicity of writing code is one of the biggest points of Swift.

Swift4Tensorflow支持很多现成的多层模型。更多阅读参考:https://www.tensorflow.org/swift/api_docs/Structs

5.6 选择梯度下降作为Optimizer

类似的,这里我们也需要选择Optimizer来优化我们的模型。我们这里选择使用随机梯度下降算法(stochastic gradient descent, SGD)。

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

Swift4Tensorflow还支持很多Optimizer:

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

5.7 模型训练

现在万事俱备了,让我们开始训练模型吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
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%以上的正确率。

5.8 可视化输出训练过程

用下面的方法我么可以可视化输出训练过程中的误差演变过程:

1
2
3
4
5
6
7
8
9
10
11
12
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()

得到的结果如下图所示:

6 Swift数据科学应用的未来

有产业专家对Swift做出了很高的评价,认为其有潜力成为数据科学的主流语言,同时也能成为机器学习类应用开发的主要工具。

目前,很多fancy的数据科学相关的Swift库还在开发中,其背后有强大的业界支持。我非常看好Swift生态的未来--甚至会比现在的Python更加强大。

下面是一些你可以进一步研究的Swift库:

本文涉及的所有代码托管在Github上