前言

这是一篇包含极少数学推导的NN入门文章

大概从今年4月份起就想着学一学NN,但是无奈平时时间不多,而且空闲时间都拿去做比赛或是看动漫去了,所以一拖再拖,直到这8月份才正式开始NN的学习。

这篇文章主要参考了《深度学习入门:基于Python的理论与实现》一书,感觉这本书很不错,偏向实践,蛮适合入门。

话不多说,下面开始我们的NN入门(手撕NN)之旅

基础数学知识

这里只对张量进行简单介绍,关于矩阵运算之类的,就靠你们自己另外学啦。

标量(0D张量)

仅包含一个数字的张量叫作标量(scalar,也叫标量张量、零维张量、0D 张量)。在 Numpy 中,一个 float32 或 float64 的数字就是一个标量张量(或标量数组)。你可以用 ndim 属性来查看一个 Numpy 张量的轴的个数。

>>> import numpy as np
>>> x = np.array(1)
>>> x
array(1)
>>> x.ndim
0

向量(1D张量)

数字组成的数组叫作向量(vector)或一维张量(1D 张量)。一维张量只有一个轴。下面是 一个 Numpy 向量。

>>> x = np.array([1, 2, 3, 4, 5])
>>> x
array([1, 2, 3, 4, 5])
>>> x.ndim
1

这个向量有5 个元素,也被称为5D 向量。

矩阵(2D张量)

向量组成的数组叫作矩阵(matrix)或二维张量(2D 张量)。矩阵有 2 个轴(通常叫作行和列),下面是一个 Numpy 矩阵。

>>> x = np.array([[5, 78, 2, 34, 0],
[6, 79, 3, 35, 1],
[7, 80, 4, 36, 2]])
>>> x.ndim
2

第一个轴上的元素叫作行(row),第二个轴上的元素叫作列(column)。在上面的例子中, [5, 78, 2, 34, 0] 是 x 的第一行,[5, 6, 7] 是第一列。

3D张量与更高维张量

将多个矩阵组合成一个新的数组,可以得到一个3D 张量,可以将其直观地理解为数字 组成的立方体。下面是一个 Numpy 的 3D 张量。

>>> x = np.array([[[5, 78, 2, 34, 0],
[6, 79, 3, 35, 1],
[7, 80, 4, 36, 2]],
[[5, 78, 2, 34, 0],
[6, 79, 3, 35, 1],
[7, 80, 4, 36, 2]],
[[5, 78, 2, 34, 0],
[6, 79, 3, 35, 1],
[7, 80, 4, 36, 2]]])
>>> x.ndim
3

将多个3D 张量组合成一个数组,可以创建一个4D 张量,以此类推。深度学习处理的一般 是 0D 到 4D 的张量,但处理视频数据时可能会遇到 5D 张量。

神经网络(Neural Network)

神经网络实际上是由多个层(神经网络的基本数据结构)堆叠而成,层是一个数据处理模块,可以将一个 或多个输入张量转换为一个或多个输出张量。下图是一个最简单的网络

这是一个三层神经网络(但实质上只有2层神经元有权重,因此也可称其为“2层网络”),包括输入层中间层(隐藏层)输出层。(个人认为,对于任意一个网络,都可以简化成上图所示的一个三层的神经网络,数据从输入层进入,经过一层运算进入隐藏层,然后在隐藏层中进行各种运算,最后再通过一层运算到达输出层,输出我们所需的结果)。

那么,对于一个最简单的网络,每一层的运算是如何的呢?

如上图所示,假设我们输入了 \(x_1, x_2\), \(x_1, x_2\) 分别乘上到下一层的权重,再加上偏置,得到一个y值,这个y值将作为下一层的输入,用公式表达如下

\[y = w_1x_1+w_2x_2+b {\tag 1}
\]

可想而知,如果所有的计算都是这样的话,那神经网络就只是一个线性模型,那要如何使其具有非线性呢?

很简单,可以加入激活函数\(h(x)\),那么,我们的公式便可改成

\[a=w_1x_1+w_2x_2+b {\tag {2.1}}
\]

\[y=h(a) {\tag {2.2}}
\]

首先,式(2.1)计算加权输入信号和偏置的总和,记为a。然后,式(2.2) 用h(x)函数将a转换为输出y。

激活函数

这里介绍下常用的激活函数

sigmoid函数

说到非线性,比较容易想到的应该是阶跃函数,比如下面代码所示的

def step_function(x):
if x > 0:
return 1
else:
return 0

但是,由于阶跃函数只有两个值,不存在平滑性,在计算过程中表示能力肯定不够好,所以,又想到sigmoid函数

def sigmoid(x):
return 1 / (1 + np.exp(-x))

sigmoid函数的平滑性对神经网络的学习具有重要意义。

ReLU函数

在神经网络发展的历史上,sigmoid函数很早就开始被使用了,而最近则主要使用ReLU(Rectified Linear Unit)函数。

\[h(x)=
\begin{cases}
x,\quad x > 0\\
0,\quad x<=0
\end{cases}
\tag{3}
\]

def relu(x):
return np.maximum(0, x)

恒等函数和softmax函数(输出层激活函数)

神经网络可以用在分类问题和回归问题上,不过需要根据情况改变输出 层的激活函数。一般而言,回归问题用恒等函数,分类问题用softmax函数。

恒等函数会将输入按原样输出,对于输入的信息,不加以任何改动地直 接输出。因此,在输出层使用恒等函数时,输入信号会原封不动地被输出。

分类问题中使用的softmax函数可以用下面的式子表示。

\[y_k = \frac{exp(a_k)}{\sum^n_{i=1}exp(a_i)} \tag{4}
\]

def softmax(a):
exp_a = np.exp(a)
sum_exp_a = np.sum(exp_a)
y = exp_a / sum_exp_a
return y

上面的softmax函数的实现在计算机的运算上存在有溢出问题。softmax函数的实现中要进行指数函数的运算,但是此时指数函数的值很容易变得非常大。比如,\(e^{10}\)的值 会超过20000,\(e^{100}\)会变成一个后面有40多个0的超大值,\(e^{1000}\)的结果会返回 一个表示无穷大的inf。如果在这些超大值之间进行除法运算,结果会出现“不确定”的情况。

因此对softmax做如下改进

def softmax(a):
c = np.max(a)
exp_a = np.exp(a - c) # 溢出对策
sum_exp_a = np.sum(exp_a)
y = exp_a / sum_exp_a
return y

网络的学习

从之前的介绍来看,设置好神经网络的参数,设置好激活函数,似乎就可以利用该神经网络来做预测了,事实也是入此。但这里存在一个很重要的问题,网络的各个权重参数如何设置?1. 人为设置,这好像就成了人工神经网络,并且十分不现实,一旦网络结构比较大,具有数万个神经元的时候,完全无法设置参数。2. 从数据中学习,这是所有机器学习、深度学习模型的一个很重要的特征,从数据中学习。

下面将介绍神经网络在学习中需要的一些东西

损失函数(loss function)

相信有机器学习基础的对此都不陌生。神经网络每次在学习时,会更新一组权重,通过这组新的权重然后产生一组预测值,那我们如何判断这组权重是否是较优的呢?通过损失函数即可,这里介绍两个损失函数(可跳过)。

损失函数是表示神经网络性能的“恶劣程度”的指标,即当前的 神经网络对监督数据在多大程度上不拟合,在多大程度上不一致。 以“性能的恶劣程度”为指标可能会使人感到不太自然,但是如 果给损失函数乘上一个负值,就可以解释为“在多大程度上不坏”, 即“性能有多好”。并且,“使性能的恶劣程度达到最小”和“使性 能的优良程度达到最大”是等价的,不管是用“恶劣程度”还是“优 良程度”,做的事情本质上都是一样的。

均方误差(mean squared error)

\[E=\frac{1}{2}\sum_k(y_k-t_k)^2 \tag{5}
\]

def mean_squared_error(y, t):
return 0.5 * np.sum((y-t)**2)

该损失函数常用于回归问题

交叉熵误差(cross entropy error)

\[E=-\sum_k{t_klogy_k} \tag{6}
\]

def cross_entropy_error(y, t):
delta = 1e-7
return -np.sum(t * np.log(y + delta))

这里,参数y和t是NumPy数组。函数内部在计算np.log时,加上了一 个微小值delta。这是因为,当出现np.log(0)时,np.log(0)会变为负无限大的-inf,这样一来就会导致后续计算无法进行。作为保护性对策,添加一个微小值可以防止负无限大的发生。

交叉熵误差常用于分类问题上

mini-batch 学习

介绍了损失函数之后,其实已经可以利用损失函数开始训练我们的神经网络了,但是,我们每次训练都不止一条数据,如果想要训练出比较好的神经网络模型,在计算损失函数时就必须将所有的训练数据作为对象。以交叉熵误差为例,损失函数改写成下面的式子

\[E=-\frac{1}{N}\sum_n\sum_kt_{nk}logy_{nk} \tag{7}
\]

但是,同时需考虑,在MNIST数据集中,训练数据有60000条,如果以全部数据为对象求损失函数的和,则计算过程需要花费较长的时间。再者,如果遇到大数据, 数据量会有几百万、几千万之多,这种情况下以全部数据为对象计算损失函数是不现实的。因此,我们从全部数据中选出一部分,作为全部数据的“近似”。神经网络的学习也是从训练数据中选出一批数据(称为mini-batch,小 批量),然后对每个mini-batch进行学习。比如,从60000个训练数据中随机选择200笔,再用这200笔数据进行学习。这种学习方式称为mini-batch学习。

此时交叉熵代码实现如下

def cross_entropy_error(y, t):
if y.ndim == 1:
t = t.reshape(1, t.size)
y = y.reshape(1, y.size) batch_size = y.shape[0]
return -np.sum(t * np.log(y + 1e-7)) / batch_size

当监督数据是标签形式(非one-hot表示,而是像“2”“ 7”这样的标签)时,交叉熵误差可通过如下代码实现。

def cross_entropy_error(y, t):
if y.ndim == 1:
t = t.reshape(1, t.size)
y = y.reshape(1, y.size)
batch_size = y.shape[0]
return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size

参数(权重和偏置)优化

上面介绍了更新权重时需要的损失函数,但是,我们要如何利用损失函数来更新权重呢?这里用到了我们熟知的梯度法。

梯度法

机器学习的主要任务是在学习时寻找最优参数。同样地,神经网络也必 须在学习时找到最优参数(权重和偏置)。这里所说的最优参数是指损失函数取最小值时的参数。但是,一般而言,损失函数很复杂,参数空间庞大,我 们不知道它在何处能取得最小值。而通过巧妙地使用梯度来寻找函数最小值 (或者尽可能小的值)的方法就是梯度法,数学表示如下

\[x_0=x_0-\eta \frac{\partial f}{\partial x_0} \\
x_1=x_1-\eta \frac{\partial f}{\partial x_1} \tag{8}
\]

式中η表示更新量,在神经网络的学习中,称为学习率(learning rate)。学习率决定在一次学习中,应该学习多少,以及在多大程度上更新参数。

def numerical_gradient(f, x):
h = 1e-4 # 0.0001
grad = np.zeros_like(x) # 生成和x形状相同的数组
it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
while not it.finished:
idx = it.multi_index
tmp_val = x[idx]
# f(x+h)的计算
x[idx] = tmp_val + h
fxh1 = f(x) # f(x-h)的计算
x[idx] = tmp_val - h
fxh2 = f(x)
grad[idx] = (fxh1 - fxh2) / (2*h) x[idx] = tmp_val # 还原值
it.iternext() return grad def gradient_descent(f, init_x, lr=0.01, step_num=100):
x = init_x
for i in range(step_num):
grad = numerical_gradient(f, x)
x -= lr * grad return x

参数f是要进行最优化的函数,init_x是初始值,lr是学习率learning rate,step_num是梯度法的重复次数。numerical_gradient(f,x)会求函数的梯度,用该梯度乘以学习率得到的值进行更新操作,由step_num指定重复的 次数。

学习率需要事先确定为某个值,比如0.01或0.001。一般而言,这个值 过大或过小,都无法抵达一个“好的位置”。在神经网络的学习中,一般会 一边改变学习率的值,一边确认学习是否正确进行了。

神经网络的梯度

\[\mathbf{W}=\left(
\begin{matrix}
w_{11} & w_{12} & w_{13} \\
w_{21} & w_{22} & w_{23}
\end{matrix}
\right) \\
\frac{\partial L}{\partial \mathbf{W}} = \left(
\begin{matrix}
\frac{\partial L}{\partial w_{11}} & \frac{\partial L}{\partial w_{12}} &\frac{\partial L}{\partial w_{13}} \\
\frac{\partial L}{\partial w_{21}} & \frac{\partial L}{\partial w_{22}} &\frac{\partial L}{\partial w_{23}}
\end{matrix}
\right) \tag{9}
\]

就是需要一个一个算比较麻烦,但是计算机就无所谓了

迭代伪代码如下

 def f(W):
return net.loss(x, t) dW = numerical_gradient(f, net.W)

学习算法的实现

根据前面的介绍,差不多可以理清神经网络的学习步骤了

  1. mini-batch

    从训练数据中随机选出一部分数据,这部分数据称为mini-batch。我们 的目标是减小mini-batch的损失函数的值。

  2. 梯度计算

    为了减小mini-batch的损失函数的值,需要求出各个权重参数的梯度。 梯度表示损失函数的值减小最多的方向。

  3. 更新参数

    将权重参数沿梯度方向进行微小更新。

  4. 迭代

    重复步骤1、步骤2、步骤3。

神经网络的学习按照上面4个步骤进行。这个方法通过梯度下降法更新参数,不过因为这里使用的数据是随机选择的mini batch数据,所以又称为 随机梯度下降法(stochastic gradient descent)

下面给出一个两层的简单神经网络的实现

class TwoLayerNet:
def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
"""
:param: input_size - 输入层的神经元数
:param: hidden_size - 隐藏层的神经元数
;param: output_size - 输出层的神经元数
"""
# 初始化权重
self.params = {}
self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size)
self.params['b1'] = np.zeros(hidden_size)
self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size) self.params['b2'] = np.zeros(output_size) def predict(self, x):
W1, W2 = self.params['W1'], self.params['W2']
b1, b2 = self.params['b1'], self.params['b2']
a1 = np.dot(x, W1) + b1
z1 = sigmoid(a1)
a2 = np.dot(z1, W2) + b2
y = softmax(a2)
return y # x:输入数据, t:监督数据
def loss(self, x, t):
y = self.predict(x)
return cross_entropy_error(y, t) def accuracy(self, x, t):
y = self.predict(x)
y = np.argmax(y, axis=1)
t = np.argmax(t, axis=1)
accuracy = np.sum(y == t) / float(x.shape[0])
return accuracy # x:输入数据, t:监督数据
def numerical_gradient(self, x, t):
loss_W = lambda W: self.loss(x, t)
grads = {}
grads['W1'] = numerical_gradient(loss_W, self.params['W1'])
grads['b1'] = numerical_gradient(loss_W, self.params['b1'])
grads['W2'] = numerical_gradient(loss_W, self.params['W2'])
grads['b2'] = numerical_gradient(loss_W, self.params['b2']) return grads

训练

# 数据加载代码
try:
import urllib.request
except ImportError:
raise ImportError('You should use Python 3.x')
import os.path
import gzip
import pickle
import os
import numpy as np
url_base = 'http://yann.lecun.com/exdb/mnist/'
key_file = {
'train_img':'train-images-idx3-ubyte.gz',
'train_label':'train-labels-idx1-ubyte.gz',
'test_img':'t10k-images-idx3-ubyte.gz',
'test_label':'t10k-labels-idx1-ubyte.gz'
} dataset_dir = os.path.dirname(os.path.abspath(__file__))
save_file = dataset_dir + "/mnist.pkl" train_num = 60000
test_num = 10000
img_dim = (1, 28, 28)
img_size = 784 def _download(file_name):
file_path = dataset_dir + "/" + file_name if os.path.exists(file_path):
return print("Downloading " + file_name + " ... ")
urllib.request.urlretrieve(url_base + file_name, file_path)
print("Done") def download_mnist():
for v in key_file.values():
_download(v) def _load_label(file_name):
file_path = dataset_dir + "/" + file_name print("Converting " + file_name + " to NumPy Array ...")
with gzip.open(file_path, 'rb') as f:
labels = np.frombuffer(f.read(), np.uint8, offset=8)
print("Done") return labels def _load_img(file_name):
file_path = dataset_dir + "/" + file_name print("Converting " + file_name + " to NumPy Array ...")
with gzip.open(file_path, 'rb') as f:
data = np.frombuffer(f.read(), np.uint8, offset=16)
data = data.reshape(-1, img_size)
print("Done") return data def _convert_numpy():
dataset = {}
dataset['train_img'] = _load_img(key_file['train_img'])
dataset['train_label'] = _load_label(key_file['train_label'])
dataset['test_img'] = _load_img(key_file['test_img'])
dataset['test_label'] = _load_label(key_file['test_label']) return dataset def init_mnist():
download_mnist()
dataset = _convert_numpy()
print("Creating pickle file ...")
with open(save_file, 'wb') as f:
pickle.dump(dataset, f, -1)
print("Done!") def _change_one_hot_label(X):
T = np.zeros((X.size, 10))
for idx, row in enumerate(T):
row[X[idx]] = 1 return T def load_mnist(normalize=True, flatten=True, one_hot_label=False):
if not os.path.exists(save_file):
init_mnist() with open(save_file, 'rb') as f:
dataset = pickle.load(f) if normalize:
for key in ('train_img', 'test_img'):
dataset[key] = dataset[key].astype(np.float32)
dataset[key] /= 255.0 if one_hot_label:
dataset['train_label'] = _change_one_hot_label(dataset['train_label'])
dataset['test_label'] = _change_one_hot_label(dataset['test_label']) if not flatten:
for key in ('train_img', 'test_img'):
dataset[key] = dataset[key].reshape(-1, 1, 28, 28) return (dataset['train_img'], dataset['train_label']), (dataset['test_img'], dataset['test_label'])
# NN训练代码
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_ laobel = True)
train_loss_list = []
train_acc_list = []
test_acc_list = []
# 平均每个epoch的重复次数
iter_per_epoch = max(train_size / batch_size, 1) # 超参数
iters_num = 10000
batch_size = 100
learning_rate = 0.1 network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10) for i in range(iters_num):
# 获取mini-batch
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
# 计算梯度
grad = network.numerical_gradient(x_batch, t_batch)
# grad = network.gradient(x_batch, t_batch) # 高速版! # 更新参数
for key in ('W1', 'b1', 'W2', 'b2'): n
etwork.params[key] -= learning_rate * grad[key]
loss = network.loss(x_batch, t_batch)
train_loss_list.append(loss)
# 计算每个epoch的识别精度
if i % iter_per_epoch == 0:
train_acc = network.accuracy(x_train, t_train)
test_acc = network.accuracy(x_test, t_test)
train_acc_list.append(train_acc)
test_acc_list.append(test_acc)
print("train acc, test acc | " + str(train_acc) + ", " + str(test_acc))

小节

这篇中介绍了NN的一些基础知识,也给出了一个用numpy实现的十分简单的一个2层神经网络的实现,将在下篇中介绍反向传播法,对现在实现的神经网络进行更进一步的优化。

本文首发于我的知乎

NN入门,手把手教你用Numpy手撕NN(一)的更多相关文章

  1. NN入门,手把手教你用Numpy手撕NN(三)

    NN入门,手把手教你用Numpy手撕NN(3) 这是一篇包含极少数学的CNN入门文章 上篇文章中简单介绍了NN的反向传播,并利用反向传播实现了一个简单的NN,在这篇文章中将介绍一下CNN. CNN C ...

  2. NN入门,手把手教你用Numpy手撕NN(2)

    这是一篇包含较少数学推导的NN入门文章 上篇文章中简单介绍了如何手撕一个NN,但其中仍有可以改进的地方,将在这篇文章中进行完善. 误差反向传播 之前的NN计算梯度是利用数值微分法,虽容易实现,但是计算 ...

  3. Pytorch入门——手把手教你MNIST手写数字识别

    MNIST手写数字识别教程 要开始带组内的小朋友了,特意出一个Pytorch教程来指导一下 [!] 这里是实战教程,默认读者已经学会了部分深度学习原理,若有不懂的地方可以先停下来查查资料 目录 MNI ...

  4. [swift实战入门]手把手教你编写2048(一)

    苹果设备越来越普及,拿着个手机就想捣鼓点啥,于是乎就有了这个系列,会一步一步教大家学习swift编程,学会自己做一个自己的app,github地址:https://github.com/scarlet ...

  5. 手把手教你学Numpy,这些api不容错过

    本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是Numpy专题的第5篇文章,我们来继续学习Numpy当中一些常用的数学和统计函数. 基本统计方法 在日常的工作当中,我们经常需要通过一 ...

  6. 手把手教你学Numpy,从此处理数据不再慌「一」

    当当当,我又开新坑了,这次的专题是Python机器学习中一个非常重要的工具包,也就是大名鼎鼎的numpy. 所以今天的文章是Numpy专题的第一篇. 俗话说得好,机器学习要想玩的溜,你可以不会写Pyt ...

  7. 手把手教你学numpy,从此数据处理不再慌【三】

    本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是numpy专题的第三篇,我们来聊聊numpy当中的索引. 上篇的末尾其实我们简单地提到了索引,但是没有过多深入.没有过多深入的原因也很 ...

  8. 手把手教你学Numpy,搞定数据处理——收官篇

    本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是Numpy专题第6篇文章,我们一起来看看Numpy库当中剩余的部分. 数组的持久化 在我们做机器学习模型的研究或者是学习的时候,在完成 ...

  9. Java入门 手把手教你配置环境变量

    很多人觉得配置Java开发的环境变量很麻烦,很容易忘记,时常被它搞得晕头转向.如果出现这样的情况,那么原因只有一个,你不了解为毛需要配置环境变量,不配置环境变量就不能开发了吗? 答案是:NO!,那么下 ...

随机推荐

  1. Python学习4——条件、循环及其他语句总结

    多种语句 打印语句: 导入语句: 赋值语句: 代码块: 条件语句: 断言: 循环: 推导: pass.dal.exec和eval :  学习到的新函数:(以下函数的应用代码均在IDLE测试通过) ch ...

  2. mui.storage 将数据持久化到本地

    在一个用mui做得app中,要求把历史记录放在本地(感觉...无法言喻的sd),但最终还是做了,以下来记录本次的学习到的内容 mui.plusReady(function() { //这里是一开始定义 ...

  3. ThinkPHP 5.0 控制器-》请求-》数据库

    ThinkPHP 5.0 控制器->请求->数据库 控制器总结 无需继承其他的类(若继承了Think/Controller,可直接调用view函数渲染模板),位置处于application ...

  4. excel表数据生成定长txt数据

    项目作业中需要造数据,从txt文件中获取定长数据,直接从txt中修改,会显得十分麻烦,于是便利用excel自带的vba写了一个小工具.效果如下: A1表示字段名,A2表示长度,A3是数据,也可以增加字 ...

  5. @ConfigurationProperties 注解使用姿势,这一篇就够了

    在编写项目代码时,我们要求更灵活的配置,更好的模块化整合.在 Spring Boot 项目中,为满足以上要求,我们将大量的参数配置在 application.properties 或 applicat ...

  6. Jquery第一次考核

    1. 什么是JS JavaScript 缩写.一种计算机脚本语言 JavaScript是一种动态.弱类型.基于原型的语言,通过浏览器可以直接执行 2. JS三大组成部件 ECMAScript DOM ...

  7. PyCharm字体大小调整

    1.点击左上角File----settings----keymap----------搜索increase,选中,increase font size--------再选择enter mouse sh ...

  8. JWT token 跨域认证

    JSON Web Token(缩写 JWT),是目前最流行的跨域认证解决方案. session登录认证方案:用户从客户端传递用户名.密码等信息,服务端认证后将信息存储在session中,将sessio ...

  9. powermockito单元测试之深入实践

    概述 由于最近工作需要, 在项目中要做单元测试, 以达到指定的测试用例覆盖率指标.项目中我们引入的powermockito来编写测试用例, JaCoCo来监控单元测试覆盖率.关于框架的选择, 网上讨论 ...

  10. 简单聊聊红黑树(Red Black Tree)

    ​ 前言 众所周知,红黑树是非常经典,也很非常重要的数据结构,自从1972年被发明以来,因为其稳定高效的特性,40多年的时间里,红黑树一直应用在许多系统组件和基础类库中,默默无闻的为我们提供服务,身边 ...