量化投资学习笔记99——创造你的第一个深度学习框架

开课吧的课程笔记,0.99元买的,老师是高民权。
第一天
不要变成那种知道很多概念但是基本功不行的人。课程的最终收获:建立自己的深度学习框架。
当你能自己创造的时候,你会彻底理解它的原理。
科研分三个类型:1.描述型。2.因果推理。3.未来预测。
最难的是预测。
案例:波士顿房价问题。
内容:
1.什么是机器学习?
2.KNN算法
3.回归算法
4.什么是损失函数,为什么它对机器学习任务很关键?
5.什么是梯度下降?
先加载数据:

1
2
from sklearn.datasets import load_boston
dataset = load_boston()

探索数据:

1
print(dataset["feature_names"])
1
2
['CRIM' 'ZN' 'INDUS' 'CHAS' 'NOX' 'RM' 'AGE' 'DIS' 'RAD' 'TAX' 'PTRATIO'
'B' 'LSTAT']

看具体描述

1
print(dataset["DESCR"])

具体输出太长就不列出来了。
看数据的值(第六列)

1
print(dataset["data"][:,5])

定义问题:假设你是一个地产销售,有人要卖房子,给出估价。
使用pandas分析处理数据。

1
2
3
4
5
6
import pandas as pd
dataframe = pd.DataFrame(dataset["data"])
dataframe.columns = dataset["feature_names"]
dataframe["price"] = dataset["target"]
print(dataframe.head())
len(dataframe)

有506列数据。
问题:什么特征对房价影响最大?
用dataframe.corr()来看特征之间的相关性。

看最后一列或最后一行。
画热点图看看。

1
2
import seaborn as sns
sns.heatmap(dataframe.corr(), annot = True, fmt = ".2f")

发现房屋卧室个数和房屋价格最成正相关。
如何依据房屋卧室的数量来估计房子面积?
将卧室数量与房屋价格做字典映射

1
2
3
X_rm = dataframe["RM"].values
Y = dataframe["price"].values
rm_to_price = {r:y for r, y in zip(X_rm, Y)}

作为一个优秀的工程师/算法工作者,代码的可读性一定是大于简洁性。
根据卧室数量找到最接近的房屋价格

1
2
3
4
5
6
7
8
9
10
import numpy as np
def find_price_by_similar(history_price, query_x, topn = 3):
# return np.mean([p for x, p in sorted(history_price.items(), key = lambda x_y: (x_y[0]-query_x)**2)[:topn]])
most_similar_items = sorted(history_price.items(), key = lambda x_y: (x_y[0]-query_x)**2)[:topn]
most_similar_prices = [price for rm, price in most_similar_items]
average_prices = np.mean(most_similar_prices)
return average_prices


find_price_by_similar(rm_to_price, 7)

rm_to_price不要写死在函数里,因为只要其值一改变函数可能就出错了。
职业与非职业的区别就在细节里。
“代码是给人看的,偶尔运行一下。”
上面就是KNN算法——K-Neighbor-Nearest
什么是机器学习?
学习是为了预测。通过观察已有数据预测未来数据。回归产生数值,分类产生类别。机器学习就是用计算机来学习。
knn算法的问题:当数据量变大时,学习时间变长。lazy learning。
一个更加有效的方法:找到X和Y之间的函数关系,每次要计算时输入给这个函数,就能直接获得预测值。
先画散点图

1
2
import matplotlib.pyplot as plt
plt.scatter(X_rm, Y)

用直线y = kx+b来拟合,如何评判拟合的“好”?
用Loss函数,即在拟合的时候信息损失了多少,因此叫损失函数。

上述损失函数称为误差平方均值(Mean Square Error, MSE)。

1
2
3
4
5
6
7
8
9
10
11
12
13
def loss(y, yhat):
return np.mean((np.array(y) - np.array(yhat))**2)


real_y = [3,6,7]
y_hats = [3,4,7]
y_hats_2 = [3,6,6]


loss(real_y, y_hats)
1.3333333333333333
loss(real_y, y_hats_2)
0.3333333333333333

因此y_hats_2要拟合得更好。
获得最优的k和b呢?
1.直接用微积分的方法计算。
最小二乘法。
当损失函数极复杂时,无法求解。
2.用随机模拟方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import random


def model(x, k, b):
return x*k+b


VAR_MAX, VAR_MIN = 100, -100
total_times = 1000
min_loss = float("inf")
best_k, best_b = None, None
for t in range(total_times):
k, b = random.randint(VAR_MIN, VAR_MAX), random.randint(VAR_MIN, VAR_MAX)
loss_ = loss(Y, model(X_rm, k, b))
# print("正在寻找....")
if loss_ < min_loss:
min_loss = loss_
best_k, best_b = k, b
print("在{}时刻我找到了更好的k:{}和b:{},此时的loss是:{}".format(t, k, b, loss_))
1
2
3
4
5
6
7
8
0时刻我找到了更好的k:-26和b:22,此时的loss是:27524.80590943083
2时刻我找到了更好的k:30和b:-13,此时的loss是:23669.676297035574
7时刻我找到了更好的k:-11和b:3,此时的loss是:8103.9628163774705
11时刻我找到了更好的k:6和b:54,此时的loss是:4833.522422316206
12时刻我找到了更好的k:7和b:-30,此时的loss是:118.71554882015812
69时刻我找到了更好的k:10和b:-35,此时的loss是:72.23144802371542
198时刻我找到了更好的k:11和b:-44,此时的loss是:52.125732583003945
615时刻我找到了更好的k:10和b:-39,此时的loss是:45.72314762845849

开始时更新很快,更新速度越来越慢。
如何更新?
k’ = k + -1×loss对k的偏导数/k的偏导数
即梯度下降。深度学习的核心,即通过梯度下降的方法,获得一组参数,使得损失函数最小。
程序实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def partial_k(x, y, k_n, b_n):
return 2*np.mean((k * x + b - y) * x)


def partial_b(x, y, k_n, b_n):
return 2*np.mean(k * x + b - y)


VAR_MAX, VAR_MIN = 100, -100
total_times = 1000
alpha = 1e-2
min_loss = float("inf")
k_b_history = []
best_k, best_b = None, None
k, b = random.randint(VAR_MIN, VAR_MAX), random.randint(VAR_MIN, VAR_MAX)
for t in range(total_times):
k, b = k+(-1)*partial_k(X_rm, Y, k, b)*alpha, b+(-1)*partial_b(X_rm, Y, k, b)*alpha
loss_ = loss(Y, model(X_rm, k, b))
# print("正在寻找....")
if loss_ < min_loss:
min_loss = loss_
best_k, best_b = k, b
print("在{}时刻我找到了更好的k:{}和b:{},此时的loss是:{}".format(t, k, b, loss_))
k_b_history.append([best_k, best_b])

结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
0时刻我找到了更好的k:17.369569754624507和b:-20.333675889328063,此时的loss是:4472.292227873009
1时刻我找到了更好的k:8.955317168137993和b:-21.65957415252766,此时的loss是:189.74333291983726
2时刻我找到了更好的k:7.437325856780592和b:-21.90134442748533,此时的loss是:50.287029325456274
3时刻我找到了更好的k:7.163792246860975和b:-21.94747888904269,此时的loss是:45.74489767191771
4时刻我找到了更好的k:7.114825887727485和b:-21.958309486748952,此时的loss是:45.596062855396596
5时刻我找到了更好的k:7.106383488125827和b:-21.962768759212416,此时的loss是:45.59029021663505
6时刻我找到了更好的k:7.1052537036267704和b:-21.966077698329645,此时的loss是:45.589176648302995
7时刻我找到了更好的k:7.105443414960139和b:-21.969178453014266,此时的loss是:45.58821524136252
8时刻我找到了更好的k:7.105871139922058和b:-21.97224103793258,此时的loss是:45.58725923424064
9时刻我找到了更好的k:7.1063417207669906和b:-21.97529613305261,此时的loss是:45.58630384763254
......
990时刻我找到了更好的k:7.525781200015785和b:-24.643413738514344,此时的loss是:44.839340912645646
991时刻我找到了更好的k:7.526160263125433和b:-24.64582500368794,此时的loss是:44.83874519542314
992时刻我找到了更好的k:7.526539235080919和b:-24.64823568901914,此时的loss是:44.83814976467313
993时刻我找到了更好的k:7.52691811590416和b:-24.65064579464738,此时的loss是:44.837554620257855
994时刻我找到了更好的k:7.527296905617073和b:-24.653055320712067,此时的loss是:44.836959762039605
995时刻我找到了更好的k:7.527675604241565和b:-24.655464267352567,此时的loss是:44.83636518988075
996时刻我找到了更好的k:7.5280542117995415和b:-24.657872634708216,此时的loss是:44.83577090364376
997时刻我找到了更好的k:7.528432728312902和b:-24.660280422918316,此时的loss是:44.83517690319111
998时刻我找到了更好的k:7.52881115380354和b:-24.662687632122132,此时的loss是:44.834583188385366
999时刻我找到了更好的k:7.529189488293343和b:-24.665094262458904,此时的loss是:44.83398975908919

基本上每次都在更新参数。
画图看看

回归比knn快得多。

第二天
从简单线性回归到复杂神经网络
从手工编码求导到自动求导。
我们只能让计算机拟合简单的线性函数。
除了线性函数,还有一种常见的函数关系是S形的函数,sigmoid函数。sigmoid(x)=1/(1+e^(-x))
画图看看

1
2
3
4
5
6
def sigmoid(x):
return 1/(1+np.exp(-x))


sub_x = np.linspace(-10,10)
plt.plot(sub_x, sigmoid(sub_x))


将其进行平移拉伸就可以变换成其它形式。

1
2
3
4
5
6
7
8
9
10
11
12
13
def sigmoid(x):
return 1/(1+np.exp(-x))


def random_line(x):
k, b = random.random(), random.random()
return k*x + b


sub_x = np.linspace(-10, 10)


plt.plot(sub_x, random_line(sigmoid(sub_x)))


画多条试试

1
2
for _ in range(5):
plt.plot(sub_x, random_line(sigmoid(sub_x)))

深度学习基本思想:用基本模块经过复合叠加来拟合复杂函数。这些基本模块就是所谓的激活函数(Active functions),其作用是让模型拟合非线性关系。没激活函数就只能拟合线性函数。
神经网络∈机器学习∈人工智能
数据量很小时,神经网络的效果不好。数据量变大时,才能使用层数超过3层的神经网络(即深度网络),使用深度网络的机器学习称为深度学习。
偏导数的求导:链式求导法则。
但是如何让计算机知道?
定义问题:给定一个模型定义,包含参数:{k1,k2,b1,b2},构建一个程序,让它能够求解k1,k2,b1,b2的偏导数是多少。
这实际上一个数据结构,图结构的问题。
用字典结构来存储各个节点和其后继节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
computing_graph = {
"k1":["L1"],
"b1":["L1"],
"x":["L1"],
"L1":['sigmoid'],
"k2":["L2"],
"b2":["L2"],
"sigmoid":["L2"],
"L2":["Loss"],
"y":["Loss"]
}
import networkx as nx
nx.draw(nx.DiGraph(computing_graph))

∂loss/∂k1 = ∂loss/∂l2 × ∂l2/∂σ × ∂σ/∂l1 × ∂l1/k1
用程序根据计算图获得输出。

1
2
3
4
5
6
7
8
def get_output(graph, node):
outputs = []
for n, links in graph.items():
if node == n:
outputs += links
return outputs
get_output(computing_graph, "k1")
['L1']

如此依次在图中找到输出节点。
如何获得k1的偏导?
获得k1的输出节点
获得k1的输出节点的输出节点
直到我们找到最后一个节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
computing_order = []
target = "k1"
computing_order.append(target)
out = get_output(computing_graph, target)[0]


while out:
computing_order.append(out)
out = get_output(computing_graph, out)
if out:
out = out[0]

computing_order

输出:

1
2
3
[('L1', 'k1'), ('sigmoid', 'L1'), ('L2', 'sigmoid'), ('Loss', 'L2')]


再输出求导顺序

1
2
3
4
5
6
order = []
for index, n in enumerate(computing_order[:-1]):
order.append((computing_order[index+1], n))
ds = "*".join(["∂{}/∂{}".format(a, b) for a, b in order[::-1]])
ds
'∂Loss/∂L2*∂L2/∂sigmoid*∂sigmoid/∂L1*∂L1/∂k1'

下面就可以求各个参数的导数了。
中间步骤可能进行超过1次,可以记录相关结果,避免重复计算。
如何让计算机自己根据计算图获得计算顺序?拓扑排序。
步骤:
1.选择一个没有进入的节点。如有多个,随机选一个。如k1
2.在图中删去上一步选择的节点,作为访问的顺序。
3.检查图是否为空。如不为空,跳至第一步。
4.若为空,将访问顺序逆序,即为求导顺序。
这个其实就是所谓的反向传播。

第三天
主要是实现前两节课的内容
一个好习惯:代码自描述。最好的文档是源代码本身。
先实现拓扑排序

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
# 拓扑排序
import random


def toplogic(graph):
sorted_nodes = []

while graph:
all_nodes_have_input = []
all_nodes_have_output = []

for n in graph:
all_nodes_have_input += graph[n]
all_nodes_have_output.append(n)

all_nodes_have_input = set(all_nodes_have_input)
all_nodes_have_output = set(all_nodes_have_output)

need_remove = all_nodes_have_output - all_nodes_have_input

if len(need_remove) > 0:
node = random.choice(list(need_remove))
need_to_visited = [node]
if len(graph) == 1:
need_to_visited += graph[node]
graph.pop(node)
sorted_nodes += need_to_visited

for _, links in graph.items():
if node in links:
links.remove(node)
else:
raise TypeError("图有回路,不能进行拓扑排序。")

return sorted_nodes


x, k, b, linear, sigmoid, y, loss = "x", "k", "b", "linear", "sigmoid", "y", "loss"


test_graph = {
x:[linear],
k:[linear],
b:[linear],
linear:[sigmoid],
sigmoid:[loss],
y:[loss]
}


print(toplogic(test_graph))
['y', 'x', 'b', 'k', 'linear', 'sigmoid', 'loss']

python3.9已经自带了拓扑排序。
下面来运用拓扑排序生成计算图。
先创建节点类。

1
2
3
4
5
6
7
8
9
10
11
class Node:
def __init__(self, inputs = [], name = None):
self.inputs = inputs
self.outputs = []
self.name = name

for n in inputs:
n.outputs.append(self)

def __repr__(self):
return self.name

再将节点转化为计算图并拓扑排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from collections import defaultdict


def convert_feed_dict_to_graph(feed_dict):
computing_graph = defaultdict(list)
nodes = [n for n in feed_dict]

while nodes:
n = nodes.pop(0)
if n in computing_graph:
continue
for m in n.outputs:
computing_graph[n].append(m)
nodes.append(m)

return computing_graph

最后测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
node_x = Node(name = "x")
node_y = Node(name = "y")
node_k = Node(name = "k")
node_b = Node(name = "b")
node_linear = Node(inputs = [node_x, node_k, node_b], name = "linear")
node_sigmoid = Node(inputs = [node_linear], name = "sigmoid")
node_loss = Node(inputs = [node_y, node_sigmoid], name = "loss")


feed_dic = {
node_x : 3,
node_y : random.random(),
node_k : random.random(),
node_b : 0.50
}


sorted_nodes = toplogic(convert_feed_dict_to_graph(feed_dic))

结果

1
[x, y, k, b, linear, sigmoid, loss]

再增加一个Placeholder类,继承Node,定于由人赋值的节点。

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
class Placeholder(Node):
def __init__(self, name = None):
Node.__init__(self, name = name)
self.name = name

# 前向传播
def forward(self):
print("我是{},人类赋值。\n".format(self.name))

def __repr__(self):
return self.name


def convert_feed_dict_to_graph(feed_dict):
computing_graph = defaultdict(list)
nodes = [n for n in feed_dict]

while nodes:
n = nodes.pop(0)
if n in computing_graph:
continue
if isinstance(n, Placeholder):
n.value = feed_dict[n]
for m in n.outputs:
computing_graph[n].append(m)
nodes.append(m)

return computing_graph


node_x = Placeholder(name = "x")
node_y = Placeholder(name = "y")
node_k = Placeholder(name = "k")
node_b = Placeholder(name = "b")
node_linear = Node(inputs = [node_x, node_k, node_b], name = "linear")
node_sigmoid = Node(inputs = [node_linear], name = "sigmoid")
node_loss = Node(inputs = [node_y, node_sigmoid], name = "loss")


feed_dic = {
node_x : 3,
node_y : random.random(),
node_k : random.random(),
node_b : 0.50
}


sorted_nodes = toplogic(convert_feed_dict_to_graph(feed_dic))
for node in sorted_nodes:
node.forward()

结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
我是b,人类赋值。


我是x,人类赋值。


我是y,人类赋值。


我是k,人类赋值。


我是linear,我没有被人类赋值,要自己计算我自己。


我是sigmoid,我没有被人类赋值,要自己计算我自己。


我是loss,我没有被人类赋值,要自己计算我自己。

再增加sigmoid等类,都是从Node继承的。

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
# 线性函数
class Linear(Node):
def __init__(self, x = None, weight = None, bias = None, name = None):
Node.__init__(self, inputs = [x, weight, bias], name = name)
self.name = name
self.value = None

# 前向传播
def forward(self):
k, x, b = self.inputs[1], self.inputs[0], self.inputs[2]
self.value = k.value * x.value + b.value
print("我是{},自己计算,值为{}。\n".format(self.name, self.value))

def __repr__(self):
return self.name

# sigmoid函数
class Sigmoid(Node):
def __init__(self, x = None, name = None):
Node.__init__(self, inputs = [x], name = name)
self.name = name
self.value = None

def _sigmoid(self, x):
return 1. / (1 + np.exp(-1 * x))

# 前向传播
def forward(self):
x = self.inputs[0]
self.value = self._sigmoid(x.value)
print("我是{},自己计算,值为{}。\n".format(self.name, self.value))

def __repr__(self):
return self.name


# Loss函数
class Loss(Node):
def __init__(self, y, yhat, name = None):
Node.__init__(self, inputs = [y, yhat], name = name)
self.name = name
self.value = None

# 前向传播
def forward(self):
y = self.inputs[0]
yhat = self.inputs[1]
self.value = np.mean(y.value - yhat.value)**2
print("我是{},自己计算,值为{}。\n".format(self.name, self.value))

def __repr__(self):
return self.name

节点的定义也改一下

1
2
3
4
5
6
7
node_x = Placeholder(name = "x")
node_y = Placeholder(name = "y")
node_k = Placeholder(name = "k")
node_b = Placeholder(name = "b")
node_linear = Linear(x = node_x, weight = node_k, bias = node_b, name = "linear")
node_sigmoid = Sigmoid(x = node_linear, name = "sigmoid")
node_loss = Loss(y = node_y, yhat = node_sigmoid, name = "loss")

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
我是y,人类赋值,值为0.712600098521853


我是x,人类赋值,值为3


我是k,人类赋值,值为0.08780973558938976


我是b,人类赋值,值为0.5


我是linear,自己计算,值为0.7634292067681693


我是sigmoid,自己计算,值为0.6820977882373328


我是loss,自己计算,值为0.0009303909326931479

现在知道了loss的值,接下来就是如何减小loss值的问题了。也就是后向传播的问题了。
下面实现反向求导。在节点类里增加backward()成员函数。
如Sigmoid类中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# sigmoid函数
class Sigmoid(Node):
def __init__(self, x = None, name = None):
Node.__init__(self, inputs = [x], name = name)
self.name = name
self.value = None

def _sigmoid(self, x):
return 1. / (1 + np.exp(-1 * x))

# 前向传播
def forward(self):
x = self.inputs[0]
self.value = self._sigmoid(x.value)
print("我是{},自己计算,值为{}。\n".format(self.name, self.value))

# 反向传播
def backward(self):
x = self.inputs[0]
self.gradients[x] = self.outputs[0].gradients[self]*self._sigmoid(x.value) * (1 - self._sigmoid(x.value))
print("self.gradients[self.inputs[0]|{}".format(self.gradients[self.inputs[0]]))

def __repr__(self):
return self.name

其它类与此类似。下面就可以进行更新步骤了。
用函数封装一下一次训练过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def forward(compute_graph):
for node in compute_graph:
node.forward()

def backward(compute_graph):
for node in compute_graph[::-1]: # 实现反向
print("我是{}".format(node.name))
node.backward()

def one_epoch(compute_graph):
forward(compute_graph)
backward(compute_graph)

# 更新步骤
def update(compute_graph):
learning_rate = 1e-1
for node in compute_graph:
if node.istrainable:
node.value = node.value-1*node.gradients[node]*learning_rate
print(node.name, node.value)

这就完成了这个过程,输入是标量,向量版本的把运算换成矩阵运算就行了。课程还介绍了怎么把库打包发布到网上给别人用。这个我就跳过了。
下面练习我就自己实现一个看看。
具体代码看这儿https://github.com/zwdnet/JSMPwork/blob/main/MyFrame.py
用波士顿房价预测作为测试问题,分别用我的框架和pytorch框架来解,并记录训练时间。用的数据,超参数都是一样的。
我的框架:
main.testMyFrame的运行时间为 : 219.15557213081047秒 框架评分:1512498.8851033389
pytorch:
main.testPytorch的运行时间为 : 358.63247944414616秒 pytorch评分:1451729.25
比pytorch时间短,评分也高一些(越低越好)……当然使用上还是pytorch更容易一些,因为我没有实现类似nn.Module的类,预测要自己写,而且跟神经网络的结构有关,改结构要改很多代码。
再来看看预测结果。
我的框架的:

第二张图是预测值与真实值之差的连线。
pytorch的。

最后,再到github上看看pytorch的源代码。nn模块的大多数类都直接/间接继承于Module类,类似我们框架的Node类。深度学习框架最核心的自动求导功能,是用C++写的,貌似在这里,而且貌似是用python能调用的形式写的。我多年不用c++了,看着一片头大。先略过吧。
学这个课程,最主要的收获是初步知道了框架实现深度学习的流程,在这个过程中,框架为我们做了哪些事。其中最关键的是反向传播使用的梯度下降法,数学原理是求导的链式法则。程序实现的原理:将计算过程抽象为图,然后采用拓扑排序的算法得到求导顺序,然后逆序依次求导。课程是直播课,老师现场敲代码,讲得很好。并没有因为是引流课程就糊弄或藏着掖着。但我并没有打算去报进阶课程,因为毕竟只是业余爱好,钱还是留着去学我的口腔专业的培训课程吧。谢谢开课吧和高民权老师的分享!
下次,再回到正题,尝试用深度学习解决我们原来的问题吧。

我发文章的三个地方,欢迎大家在朋友圈等地方分享,欢迎点“在看”。
我的个人博客地址:https://zwdnet.github.io
我的知乎文章地址: https://www.zhihu.com/people/zhao-you-min/posts
我的微信个人订阅号:赵瑜敏的口腔医学学习园地

欢迎打赏!感谢支持!