导入 autograd
库,同时导入这个库里的numpy
(应该是作者自己把numpy
放入了这个库的命名空间里面)以及逐项求导elementwise_grad
。
from autograd import grad
import autograd.numpy as np
from autograd import elementwise_grad
接下来定义第一个函数,这个函数非常简单,其实就是一个线性变换:
y n × l = X n × d w d × l \mathbf{y}_{n\times l} = \mathbf{X}_{n\times d} \mathbf{ w} _{d\times l} yn×l=Xn×dwd×l
这个变换也常用于神经网络的输入层。
def lin_func(x,w):
return np.dot(x,w)
x = np.array([[1,2,3],[4,5,6]])
w = np.array([1,1,1]).astype(float)
print(lin_func(x,w))
[ 6. 15.]
这里也要注意一个问题:
autograd
并不能支持numpy
的所有操作。比如上面的线性变换其实也可以用x.dot(w)
来实现,但这种方式下就不能再用autograd
自动求导了。因此在使用时也要注意看看文档。
接下来写出这个函数的导数 ∂ y ∂ X \frac{\partial \mathbf{y}}{\partial \mathbf{X}} ∂X∂y。
用法很简单,直接将函数名作为"参数"传入 elementwise_grad
即可,得到的仍然是一个callable
的类型,这里命名为 lin_grads
:
lin_grads = elementwise_grad(lin_func)
dx = lin_grads(x,w)
print(dx)
print(dx.shape)
[[1 1 1]
[1 1 1]]
(2, 3)
这里其实存在一个问题,如果我们用标量求导来类推,那么此时导数应该是:
∂ y ∂ X = w T \frac{\partial \mathbf{y}}{\partial \mathbf{X}} =\mathbf{w}^T ∂X∂y=wT
而刚刚我们设好的 w \mathbf{w} w 是 3 × 1 3 \times 1 3×1 维,那么上面的导数就应该是 1 × 3 1 \times 3 1×3维,而此时得到的结果却是 2 × 3 2 \times 3 2×3 维。这一点和我们直观的认识是不一样的。因为在矩阵对矩阵求导时,许多在标量求导时的结论是不存在的。 而如果要统一这些运算,则需要考虑 Kronecker 乘积。这个问题就相对复杂了,有空可以去看看这个:Kronecker Products and Matrix Calculus in System Theory
这里最简单的解释是:刚刚的导数,对于 X \mathbf{X} X 的每一行其实都是 w T \mathbf{w}^T wT,而此时 X \mathbf{X} X 有2行,因此结果出现了2列。
紧接着另外一个问题是:这个库求导时,只是对elementwise_grad
中的第一个参数求导,下面这个例子演示了
∂
y
∂
w
\frac{\partial \mathbf{y}}{\partial \mathbf{w}}
∂w∂y 的计算结果:
def lin_wx(w,x):
return np.dot(x,w)
print(lin_wx(w,x))
[ 6. 15.]
lin_grad_wx = elementwise_grad(lin_wx)
dw = lin_grad_wx(w,x)
print(dw)
print(dw.shape)
[5. 7. 9.]
(3,)
这里其实又出现了一个“反常”的现象:同样地,如果按照标量求导的思路来看,此时的导数应该是:
∂ y ∂ w = X T \frac{\partial \mathbf{y}}{\partial \mathbf{w}} =\mathbf{X}^T ∂w∂y=XT
而事实却是:
∂ y ∂ w = ∑ i = 1 n X i , : T \frac{\partial \mathbf{y}}{\partial \mathbf{w}} = \sum_{i=1}^{n} \mathbf{X}_{i,:}^T ∂w∂y=i=1∑nXi,:T
这里 X i , : \mathbf{X}_{i,:} Xi,: 是 X \mathbf{X} X 的第 i i i 行。 如果要想彻底弄明白这个问题还是建议去看这个:Kronecker Products and Matrix Calculus in System Theory。
当然,通常而言,我们只需要考虑 loss
对某个参数的导数就可以了,这个库也做到了这一点:即对某个标量函数,其实是完全可以得到与某参数的shape
一模一样的导数,这样在调用梯度做训练时也就比较简单了。
以下是官方的一个完整的logistic regression
的例子。
from builtins import range
import autograd.numpy as np
from autograd import grad
from autograd.test_util import check_grads
def sigmoid(x):
return 0.5*(np.tanh(x) + 1)
def logistic_predictions(weights, inputs):
# Outputs probability of a label being true according to logistic model.
return sigmoid(np.dot(inputs, weights))
def training_loss(weights):
# Training loss is the negative log-likelihood of the training labels.
preds = logistic_predictions(weights, inputs)
label_probabilities = preds * targets + (1 - preds) * (1 - targets)
return -np.sum(np.log(label_probabilities))
# Build a toy dataset.
inputs = np.array([[0.52, 1.12, 0.77],
[0.88, -1.08, 0.15],
[0.52, 0.06, -1.30],
[0.74, -2.49, 1.39]])
targets = np.array([True, True, False, True])
# Build a function that returns gradients of training loss using autograd.
training_gradient_fun = grad(training_loss)
# Check the gradients numerically, just to be safe.
weights = np.array([0.0, 0.0, 0.0])
check_grads(training_loss, modes=['rev'])(weights)
# Optimize weights using gradient descent.
print("Initial loss:", training_loss(weights))
for i in range(100):
weights -= training_gradient_fun(weights) * 0.01
print("Trained loss:", training_loss(weights))
Initial loss: 2.772588722239781
Trained loss: 0.38900754315581143