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 Howard1为一个语言背书,且将这门语言应用到他的日常数据科学研究中时,你就应该暂时停止你手上的工作好好听一听了。

在这这篇文章中我们将学习 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 上


  1. Howard was the President and Chief Scientist at Kaggle↩︎