基於約束的程式生成
# -*- 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
()
(
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
()
(
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
()
(
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
})
(
loss_value
)
在 pytorch 的版本里,可以在任意地方新增列印的程式碼,把對應的tensor打印出來。但是對於 tensorflow 的版本,所謂的程式碼,例如 loss = tf。reduce_sum((y - y_pred)**2。0) 都只是一個對靜態計算的宣告。這裡是沒有辦法去列印 loss 這個變數的。
上圖形象地表明瞭 pytorch 是如何動態地構建計算圖的過程。利用 pytorch 可以讓這個圖在每次迭代的時候都稍微有一些不同,也可以很容易控制每次迭代的步長。因為整個計算過程就是一個“很熟悉”的 for 迴圈。這個使用者介面(for迴圈)相比tensorflow自己搞了一門新的程式語言,對開發者更友好。
除錯
Direct Manipulation 的錯覺的核心在於可觸碰。在程式設計裡的可觸碰就是可除錯。可以在任意行停住,檢查每個變數的值。如果執行期乾的事情和程式設計的程式碼區別太大的時候,就沒法制造 Direct Manipulation 的錯覺了。
因為 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
只操作和感知
同層的抽象
tensorflow 的問題它有兩層的抽象。一層是程式碼表示的計算圖,一層是執行時實際的tensor。問題是程式設計師同時要面對這兩層抽象。在寫程式碼的時候操作的是計算圖這一層,在除錯的時候感知到的是tensor這一層。這中間的對映關係是需要依靠大腦的 working memory 去腦補的。腦容量不夠的開發者就會覺得很痛苦。
這個現象一個生活中的例子是冰箱的溫度調節
冰箱分為兩個艙室。一個是冷凍,一個是冷藏。
然後冰箱有兩個調節器
一個自然的想法,是一個調節器控制的是冷藏的溫度,一個調節器控制的是冷凍的溫度。然而不是這樣的
一個控制了總的製冷風量,一個是控制了上下兩個倉對冷風的分配比例。這個問題就是使用者操作和感知的是兩層的抽象。當用戶不知道這中間的轉換關係,或者這個關係很複雜的時候,就會感到非常難掌控。調節投影儀的橫向縱向的變形比例也是類似的體驗,非常痛苦。