基於約束的程式生成

# -*- coding: utf-8 -*-

import

numpy

as

np

# N is batch size; D_in is input dimension;

# H is hidden dimension; D_out is output dimension。

N

D_in

H

D_out

=

64

1000

100

10

# Create random input and output data

x

=

np

random

randn

N

D_in

y

=

np

random

randn

N

D_out

# Randomly initialize weights

w1

=

np

random

randn

D_in

H

w2

=

np

random

randn

H

D_out

learning_rate

=

1e-6

for

t

in

range

500

):

# Forward pass: compute predicted y

h

=

x

dot

w1

h_relu

=

np

maximum

h

0

y_pred

=

h_relu

dot

w2

# Compute and print loss

loss

=

np

square

y_pred

-

y

sum

()

print

t

loss

# Backprop to compute gradients of w1 and w2 with respect to loss

grad_y_pred

=

2。0

*

y_pred

-

y

grad_w2

=

h_relu

T

dot

grad_y_pred

grad_h_relu

=

grad_y_pred

dot

w2

T

grad_h

=

grad_h_relu

copy

()

grad_h

h

<

0

=

0

grad_w1

=

x

T

dot

grad_h

# Update weights

w1

-=

learning_rate

*

grad_w1

w2

-=

learning_rate

*

grad_w2

這段程式碼本質上就是在生成一個函式 f(),其定義是

//

f

()

definition

h

=

x

dot

w1

h_relu

=

np

maximum

h

0

reutrn

h_relu

dot

w2

其中 w1 和 w2 是可以變的部分。約束是

給定 輸入 x

給定 預期的輸出 y

f() 中有 dot 和 maximum 等操作構成的模板,只有 w1 和 w2 是可變的部分

執行完之後,w1 和 w2 被計算出來。完整的 f() 的函式定義也就出來了。這個原理應用到複雜的場景下,例如從影象中檢出目標物體,這個函式就是用來判別預期的模式是不是在影象的畫素中存在的函式。

也就是不是程式設計師自己親自“編”程,而是程式設計師提供約束,讓計算機自己去“編”程。類似的做法也有一些,目標是讓程式設計師不用指定algorithm的全部,從而“偷懶”:

prolog 等規則系統,程式設計師提供約束,計算機進行求解

k8s 程式設計師定義預期的叢集狀態,k8s自己去和叢集的實際情況做diff,求解出需要做的action

SQL 宣告輸入輸出兩張表的關係,執行計劃求解出最佳的執行方案

SQL 宣告 materalized view 和原始表的關係,資料自動保持兩張表的同步

# -*- coding: utf-8 -*-

import

torch

dtype

=

torch

float

device

=

torch

device

“cpu”

# device = torch。device(“cuda:0”) # Uncomment this to run on GPU

# N is batch size; D_in is input dimension;

# H is hidden dimension; D_out is output dimension。

N

D_in

H

D_out

=

64

1000

100

10

# Create random input and output data

x

=

torch

randn

N

D_in

device

=

device

dtype

=

dtype

y

=

torch

randn

N

D_out

device

=

device

dtype

=

dtype

# Randomly initialize weights

w1

=

torch

randn

D_in

H

device

=

device

dtype

=

dtype

w2

=

torch

randn

H

D_out

device

=

device

dtype

=

dtype

learning_rate

=

1e-6

for

t

in

range

500

):

# Forward pass: compute predicted y

h

=

x

mm

w1

h_relu

=

h

clamp

min

=

0

y_pred

=

h_relu

mm

w2

# Compute and print loss

loss

=

y_pred

-

y

pow

2

sum

()

item

()

print

t

loss

# Backprop to compute gradients of w1 and w2 with respect to loss

grad_y_pred

=

2。0

*

y_pred

-

y

grad_w2

=

h_relu

t

()

mm

grad_y_pred

grad_h_relu

=

grad_y_pred

mm

w2

t

())

grad_h

=

grad_h_relu

clone

()

grad_h

h

<

0

=

0

grad_w1

=

x

t

()

mm

grad_h

# Update weights using gradient descent

w1

-=

learning_rate

*

grad_w1

w2

-=

learning_rate

*

grad_w2

pytorch 和 numpy 寫法上是一樣的。就是提供 tensor 和操作 tensor 的原子函式,然後讓使用者自己去組裝。唯一的區別是 pytorch 支援同樣的邏輯,用 cpu 和 gpu 兩種不同的實現去執行。

Direct Manipulation

pytorch 相比 tensorflow (非 eager 模式)的顯著區別是,pytorch 函式描述的是被執行的“東西”本身。當然所有這種 Direct Manipulation 都是錯覺,除非你直接用 CPU/GPU 的 ISA 指令去寫程式碼。所有的程式碼都需要轉換的。但是有的轉換,讓你感覺到仍然是Direct Manipulation,有的轉換,就會有膈應的“存在感”。在兩個方面,pytorch 顯著優於 tensorflow 提供的程式設計模型,給人更直接了當的感覺:

構建動態的計算圖

pytorch 版本

# -*- coding: utf-8 -*-

import

torch

class

MyReLU

torch

autograd

Function

):

“”“

We can implement our own custom autograd Functions by subclassing

torch。autograd。Function and implementing the forward and backward passes

which operate on Tensors。

”“”

@staticmethod

def

forward

ctx

input

):

“”“

In the forward pass we receive a Tensor containing the input and return

a Tensor containing the output。 ctx is a context object that can be used

to stash information for backward computation。 You can cache arbitrary

objects for use in the backward pass using the ctx。save_for_backward method。

”“”

ctx

save_for_backward

input

return

input

clamp

min

=

0

@staticmethod

def

backward

ctx

grad_output

):

“”“

In the backward pass we receive a Tensor containing the gradient of the loss

with respect to the output, and we need to compute the gradient of the loss

with respect to the input。

”“”

input

=

ctx

saved_tensors

grad_input

=

grad_output

clone

()

grad_input

input

<

0

=

0

return

grad_input

dtype

=

torch

float

device

=

torch

device

“cpu”

# device = torch。device(“cuda:0”) # Uncomment this to run on GPU

# N is batch size; D_in is input dimension;

# H is hidden dimension; D_out is output dimension。

N

D_in

H

D_out

=

64

1000

100

10

# Create random Tensors to hold input and outputs。

x

=

torch

randn

N

D_in

device

=

device

dtype

=

dtype

y

=

torch

randn

N

D_out

device

=

device

dtype

=

dtype

# Create random Tensors for weights。

w1

=

torch

randn

D_in

H

device

=

device

dtype

=

dtype

requires_grad

=

True

w2

=

torch

randn

H

D_out

device

=

device

dtype

=

dtype

requires_grad

=

True

learning_rate

=

1e-6

for

t

in

range

500

):

# To apply our Function, we use Function。apply method。 We alias this as ‘relu’。

relu

=

MyReLU

apply

# Forward pass: compute predicted y using operations; we compute

# ReLU using our custom autograd operation。

y_pred

=

relu

x

mm

w1

))

mm

w2

# Compute and print loss

loss

=

y_pred

-

y

pow

2

sum

()

print

t

loss

item

())

# Use autograd to compute the backward pass。

loss

backward

()

# Update weights using gradient descent

with

torch

no_grad

():

w1

-=

learning_rate

*

w1

grad

w2

-=

learning_rate

*

w2

grad

# Manually zero the gradients after updating weights

w1

grad

zero_

()

w2

grad

zero_

()

tensorflow 版本

# -*- coding: utf-8 -*-

import

tensorflow

as

tf

import

numpy

as

np

# First we set up the computational graph:

# N is batch size; D_in is input dimension;

# H is hidden dimension; D_out is output dimension。

N

D_in

H

D_out

=

64

1000

100

10

# Create placeholders for the input and target data; these will be filled

# with real data when we execute the graph。

x

=

tf

placeholder

tf

float32

shape

=

None

D_in

))

y

=

tf

placeholder

tf

float32

shape

=

None

D_out

))

# Create Variables for the weights and initialize them with random data。

# A TensorFlow Variable persists its value across executions of the graph。

w1

=

tf

Variable

tf

random_normal

((

D_in

H

)))

w2

=

tf

Variable

tf

random_normal

((

H

D_out

)))

# Forward pass: Compute the predicted y using operations on TensorFlow Tensors。

# Note that this code does not actually perform any numeric operations; it

# merely sets up the computational graph that we will later execute。

h

=

tf

matmul

x

w1

h_relu

=

tf

maximum

h

tf

zeros

1

))

y_pred

=

tf

matmul

h_relu

w2

# Compute loss using operations on TensorFlow Tensors

loss

=

tf

reduce_sum

((

y

-

y_pred

**

2。0

# Compute gradient of the loss with respect to w1 and w2。

grad_w1

grad_w2

=

tf

gradients

loss

w1

w2

])

# Update the weights using gradient descent。 To actually update the weights

# we need to evaluate new_w1 and new_w2 when executing the graph。 Note that

# in TensorFlow the the act of updating the value of the weights is part of

# the computational graph; in PyTorch this happens outside the computational

# graph。

learning_rate

=

1e-6

new_w1

=

w1

assign

w1

-

learning_rate

*

grad_w1

new_w2

=

w2

assign

w2

-

learning_rate

*

grad_w2

# Now we have built our computational graph, so we enter a TensorFlow session to

# actually execute the graph。

with

tf

Session

()

as

sess

# Run the graph once to initialize the Variables w1 and w2。

sess

run

tf

global_variables_initializer

())

# Create numpy arrays holding the actual data for the inputs x and targets

# y

x_value

=

np

random

randn

N

D_in

y_value

=

np

random

randn

N

D_out

for

_

in

range

500

):

# Execute the graph many times。 Each time it executes we want to bind

# x_value to x and y_value to y, specified with the feed_dict argument。

# Each time we execute the graph we want to compute the values for loss,

# new_w1, and new_w2; the values of these Tensors are returned as numpy

# arrays。

loss_value

_

_

=

sess

run

([

loss

new_w1

new_w2

],

feed_dict

=

{

x

x_value

y

y_value

})

print

loss_value

在 pytorch 的版本里,可以在任意地方新增列印的程式碼,把對應的tensor打印出來。但是對於 tensorflow 的版本,所謂的程式碼,例如 loss = tf。reduce_sum((y - y_pred)**2。0) 都只是一個對靜態計算的宣告。這裡是沒有辦法去列印 loss 這個變數的。

PyTorch 的程式設計模型優點

上圖形象地表明瞭 pytorch 是如何動態地構建計算圖的過程。利用 pytorch 可以讓這個圖在每次迭代的時候都稍微有一些不同,也可以很容易控制每次迭代的步長。因為整個計算過程就是一個“很熟悉”的 for 迴圈。這個使用者介面(for迴圈)相比tensorflow自己搞了一門新的程式語言,對開發者更友好。

除錯

Direct Manipulation 的錯覺的核心在於可觸碰。在程式設計裡的可觸碰就是可除錯。可以在任意行停住,檢查每個變數的值。如果執行期乾的事情和程式設計的程式碼區別太大的時候,就沒法制造 Direct Manipulation 的錯覺了。

PyTorch 的程式設計模型優點

PyTorch 的程式設計模型優點

PyTorch 的程式設計模型優點

因為 tensorflow 自己發明了一套語言。所以無法使用python本身的變數名做為tensor的標識。必須在 python 變數名之外,額外再給 tensor 進行命名,才能再執行時從debugger裡找到對應的變數。

pytorch 模式的缺陷

pytorch 的模式是複用了 python 程式語言的執行模型,這導致了兩個缺點

python 自身是執行在 cpu 上的,無法把整個 python 函式整體變成一個 GPU Kernel 整體放入 GPU 執行,必須頻繁切換回 CPU

python 程式碼未被執行到的部分是不知道有什麼的。餘下計算還有哪些這個關鍵資訊的缺失,這就導致無法提前做一些區域性最佳化。例如 ORM 裡的 N+1 SELECT 問題也是類似的。

tracing

# This will run your nn。Module or regular Python function with the example

# input that you provided。 The returned callable can be used to re-execute

# all operations that happened during the example run, but it will no longer

# use the Python interpreter。

from torch。jit import trace

traced_model = trace(model, example_input=input)

traced_fn = trace(fn, example_input=input)

# The training loop doesn‘t change。 Traced model behaves exactly like an

# nn。Module, except that you can’t edit what it does or change its attributes。

# Think of it as a “frozen module”。

for input, target in data_loader:

loss = loss_fn(traced_model(input), target)

透過記錄動態執行的過程,還原這個計算圖。從而可以把計算圖靜態匯出給其他使用靜態圖的executor(例如tensorflow)去使用

jit

一個解決方案就是在執行時對 python 程式碼進行編譯,整體轉換成另外一種語言進行執行

import torch

@torch。jit。script

def foo(x, y):

if x。max() > y。max():

r = x

else:

r = y

return r

這個做法在 python numba 等框架裡也有見過。看起來是 python,其實是一門類 python 的語言了。

aot

和 jit 相對應的就是 ahead of time compiler 了。和 jit 的主要區別是用不用執行時獲得的統計資訊,去最佳化編譯的選擇。

投機執行

另外一個解決“不知道前面還有啥要計算的”這個問題的解法就是投機執行。例如我們的 CPU 就可以在執行之前先看看後面還有哪些指令集要做,提前去做一些資料準備的事情。甚至會提前做好計算。

facebook 的 haxl 在應用層也實現了類似的投機執行的機制。

為什麼 pytorch 的模式更開發者友好

兩點原因

複用了已有的 python 的程式設計 API,加法就是加法,減法就是減法,迴圈就是 for

只操作和感知

同層的抽象

PyTorch 的程式設計模型優點

tensorflow 的問題它有兩層的抽象。一層是程式碼表示的計算圖,一層是執行時實際的tensor。問題是程式設計師同時要面對這兩層抽象。在寫程式碼的時候操作的是計算圖這一層,在除錯的時候感知到的是tensor這一層。這中間的對映關係是需要依靠大腦的 working memory 去腦補的。腦容量不夠的開發者就會覺得很痛苦。

這個現象一個生活中的例子是冰箱的溫度調節

冰箱分為兩個艙室。一個是冷凍,一個是冷藏。

PyTorch 的程式設計模型優點

然後冰箱有兩個調節器

PyTorch 的程式設計模型優點

一個自然的想法,是一個調節器控制的是冷藏的溫度,一個調節器控制的是冷凍的溫度。然而不是這樣的

PyTorch 的程式設計模型優點

一個控制了總的製冷風量,一個是控制了上下兩個倉對冷風的分配比例。這個問題就是使用者操作和感知的是兩層的抽象。當用戶不知道這中間的轉換關係,或者這個關係很複雜的時候,就會感到非常難掌控。調節投影儀的橫向縱向的變形比例也是類似的體驗,非常痛苦。