知也无涯

吾生也有涯,而知也无涯,能学一点算一点…

  • 在开始之前,也没想到99行代码就够了,原以为里面有个“梯度下降”,代码行数应该是数百级别吧,实际完成后,发现加上注释(约35%)才99行。


    “极简”神经网络的结构

    先来看“极简”有多简:

    • 一个输入层、一个输出层,中间没有隐藏层
    • 输入层的样本数据就一个特性(feature),总计6个样本
    • 这是二分类问题,所以输出层就一个输出值
       x ->   w*x + b   ->   logistic function  -> output
            ----------------------------------
                 |                    |
                 V                    V
             one neuron     activation function

    在后续的实现中,我们构造了六个样本用于该神经网络的训练。

    鉴于这个“极简”神经网络,没有任何隐藏层,所以,这也是一个典型的“logistic regression”问题。

    前置知识

    你需要了解如下的前置知识,以很好的理解该神经网络的实现与训练:

    • 了解神经网络的基础:浅层神经网络
    • 了解 梯度下降算法,了解基本最优化算法概念,了解链式法则
    • 了解 logistic function 的基本特性
    • 了解 Python 和 NumPy 的基本使用

    问题描述与符号约定

    用于训练的样本数据有\( (x,\hat{y}) \): \( (1,0)、(2,0)、(3,0)、(4,0)、(5,1)、(6,1) \)。

    一个具体的样本,在下面的公式中通常使用 \( (x^{(j)}, y^{(j)}) \)表示, 其中,\( j = 1…m \)。

    \( \hat{y} \)则表示根据参数计算出的预测值,更为具体的 \( \hat{y}^{(j)} \)表示 \(x = x^{(j)} \)时的预测值。

    构建目标函数

    从样本数据可以看到,这是一个二分类问题,可以使用logistic function作为输出层的激活函数,如果输出值大于0.5,则预测为1,否则预测为0

    对于任何一个样本,就可以如下函数作为logistic function的损失函数\( L \):

    $$
    L(y,\hat{y}) = – (yln(\hat{y}) + (1-y)ln(1-\hat{y}))
    $$

    所以,全局的目标函数就是:

    $$
    \begin{aligned}
    J(w,b) & = \frac{1}{m} \sum\limits_{j=0}^{m} L(y^{(j)},\hat{y}^{(j)}) \\
    & = \frac{1}{m} \sum\limits_{j=0}^{m} – (y^{(j)}ln(\hat{y}^{(j)}) + (1-y^{(j)})ln(1-\hat{y}^{(j)}))
    \end{aligned}
    $$

    其中 \( m \)表示总样本数量,这里取值是6。在这个极简的神经网络中 \( \hat{y} \)有如下计算表达式:

    $$
    \hat{y} = \frac{1}{1+e^{-(wx+b)}}
    $$

    最终,该神经网络的参数求解(也就是“训练”)过程,就是求解如下的极值问题:

    $$
    (w,b) = \min_{w, b} J(w, b) = \min_{w,b} \frac{1}{m} \sum\limits_{j=0}^{m} L(y^{(j)},\hat{y}^{(j)})
    $$

    目标函数计算的具体代码如下:

    def cost_function(w_p,b_p,x_p,y_p):
        c = 0
        for i in range(m):
            y = function_f(x_p[i],w_p,b_p)
            c += -y_p[i]*math.log(y) - (1-y_p[i])*math.log(1-y)
        return c

    梯度计算

    前面介绍了很多梯度的内容,这里不再详述。在这个具体的问题中,需要求解的梯度为:

    $$
    (\frac{\partial J}{\partial w},\frac{\partial J}{\partial b})
    $$

    在这里,简单展示该梯度的计算,主要需要使用的是链式法则和基本的求导/微分运算。

    首先,为了便于计算,这里记:

    $$
    \begin{aligned}
    \hat{y} & = \frac{1}{1+e^{-z}} \\
    z & = w*x + b
    \end{aligned}
    $$

    所以,根据链式法则容易有:

    $$
    \frac{\partial L}{\partial w} = \frac{\partial L}{\partial \hat{y}} * \frac{\partial \hat{y}}{\partial z} * \frac{\partial z}{\partial w} \\
    \frac{\partial L}{\partial b} = \frac{\partial L}{\partial \hat{y}} * \frac{\partial \hat{y}}{\partial z} * \frac{\partial z}{\partial b}
    $$

    这其中,\( \frac{\partial L}{\partial \hat{y}} \) 和 \( \frac{\partial \hat{y}}{\partial z} \)略有一些计算量,\( \frac{\partial z}{\partial w} \) 和\( \frac{\partial z}{\partial b} \)比较简单,具体的:

    $$
    \begin{aligned}
    \frac{\partial L}{\partial \hat{y}} * \frac{\partial \hat{y}}{\partial z} & = \hat{y} – y \\
    \frac{\partial z}{\partial w} & = x \\
    \frac{\partial z}{\partial b} & = 1
    \end{aligned}
    $$

    所以,最终的梯度计算公式如下:

    $$
    \begin{aligned}
    \frac{\partial J}{\partial w} & = \frac{1}{m} \sum\limits_{j=1}^{m} (\hat{y}^{(j)} – y^{(j)})*x^{(j)} \\
    \frac{\partial J}{\partial b} & = \frac{1}{m} \sum\limits_{j=1}^{m} (\hat{y}^{(j)} – y^{(j)})
    \end{aligned}
    $$

    在实际的计算中,先通过正向传播(Forward Propagation)计算出\( \hat{y}^{(j)} \),然后在计算出梯度。此外,可以使用NumPyndarray简化表达,同时增加计算的并行性。这里为便于理解,全部都使用标量计算,在文章的最后也提供了NumPy的对应实现。

    正向传播计算如下:

    # function_f: 
    # x   : scalar
    # w   : scalar
    # b   : scalar
    def function_f(x,w,b):  
        return 1/(1+math.exp(-(x*w+b)))

    梯度(反向传播)计算如下:

    # Gradient caculate 
    # x_p: x_train
    # y_p: y_train
    # w_p: current w
    # b_p: current b
    def gradient_caculate(x_p,y_p,w_p,b_p):
        gradient_w,gradient_b = (0.,0.)
        for i in range(m):
            gradient_w += x_p[i]*(function_f(x_p[i],w_p,b_p)-y_p[i])
            gradient_b += function_f(x_p[i],w_p,b_p)-y_p[i]
        return gradient_w,gradient_b

    梯度下降迭代

    这里设置迭代次数为50000次,学习率设置为0.01,当迭代目标函数变化值小于0.000001时也停止迭代(这并不是必须的)。具体的:

    iteration_count = 50000
    learning_rate = 0.01
    cost_reduce_threshold = 0.000001

    于是又如下梯度下降迭代过程的代码:

    cost_last = 0
    for i in range(iteration_count):
        grad_w,grad_b = gradient_caculate(x_train,y_train,w,b)
        w = w - learning_rate*grad_w
        b = b - learning_rate*grad_b
        cost_current = cost_function(w,b,x_train,y_train)
        if i >= iteration_count/2 and cost_last - cost_current<= cost_reduce_threshold:
            print("iteration: {:5d},cost_current:{:f},cost_last:{:f},cost reduce:{:f}".format( i+1,cost_current,cost_last,cost_last-cost_current))
            break
        if (i+1)%(iteration_count/10) == 0:
            print("iteration: {:5d},cost_current:{:f},cost_last:{:f},cost reduce:{:f}".format( i+1,cost_current,cost_last,cost_last-cost_current))
        cost_last = cost_current

    预测

    完成训练后,则可以对输入值进行预测。代码如下:

    print("after the training, parameter w = {:f} and b = {:f}".format(w,b))
    
    for i in range(m):
        y = function_f(x_train[i],w,b)
        p  = 0
        if y>= 0.5: p  = 1
        print("sample: x[{:d}]:{:d},y[{:d}]:{:d}; the prediction is {:d} with probability:{:4f}".format(i,x_train[i],i,y_train[i],p,y))

    上述代码会产生如下的输出:

    after the training, parameter w = 5.056985 and b = -22.644516
    sample: x[0]:0,y[0]:0; the prediction is 0 with probability:0.000000
    sample: x[1]:1,y[1]:0; the prediction is 0 with probability:0.000000
    sample: x[2]:2,y[2]:0; the prediction is 0 with probability:0.000004
    sample: x[3]:3,y[3]:0; the prediction is 0 with probability:0.000568
    sample: x[4]:4,y[4]:0; the prediction is 0 with probability:0.081917
    sample: x[5]:5,y[5]:1; the prediction is 1 with probability:0.933417

    可以看到,在完成训练后的这个极简神经网络能够较为准确的预测样本中的数据。

    完整的代码

    完成在的代码可以在 GitHub 上查看:https://github.com/orczhou/ssnn/ 。包括了三个段程序:

    • ssnn_ant.py : 最为基础的上述神经网络的实现
    • ssnn_ant_np.py : 使用numpy对上述实现进行向量化
    • ssnn_ant_tf.py : 使用 TensorFlow 框架实现上述程序

    这里也简单列出相关程序如下(最新代码可以参考上述 GitHub 仓库):

    ssnn_ant.py
    """
    super simple neural networks 
      * only one neuron in only the one hidden layer
      * input x is scalar (one-dimension)
      * using logistic function as the activation function
    
    input layer:
        x: scalar 
    parameters: 
        w: scalar
        b: scalar
    output:
        y \in [0,1] or  p \in {0,1}
    structure:
             
       x ->   w*x + b   ->   logistic function  -> output
            -----------      -----------------
                 |                    |
                 V                    V
             one neuron     activation function
    
    about it:
        it's a simple project for human learning how machine learning 
        by orczhou.com
    """
    import numpy as np
    import math
    
    # function_f: 
    # x   : scalar
    # w   : scalar
    # b   : scalar
    def function_f(x,w,b):  
        return 1/(1+math.exp(-(x*w+b)))
    
    # initial w,b
    w,b = (0,0)
    
    # samples
    x_train = np.array([0,1,2,3,4,5])
    y_train = np.array([0,0,0,0,0,1])
    #y_train = np.array([0,0,0,1,1,1])
    
    # m for sample counts
    m = x_train.shape[0]
    
    iteration_count = 50000
    learning_rate   = 0.01
    cost_reduce_threshold = 0.000001
    
    # Gradient caculate 
    # x_p: x_train
    # y_p: y_train
    # w_p: current w
    # b_p: current b
    def gradient_caculate(x_p,y_p,w_p,b_p):
        gradient_w,gradient_b = (0.,0.)
        for i in range(m):
            gradient_w += x_p[i]*(function_f(x_p[i],w_p,b_p)-y_p[i])
            gradient_b += function_f(x_p[i],w_p,b_p)-y_p[i]
        return gradient_w,gradient_b
    
    def cost_function(w_p,b_p,x_p,y_p):
        c = 0
        for i in range(m):
            y = function_f(x_p[i],w_p,b_p)
            c += -y_p[i]*math.log(y) - (1-y_p[i])*math.log(1-y)
        return c
    
    about_the_train = '''\
    try to train the model with:
      learning rate: {:f}
      max iteration : {:d}
      cost reduction threshold: {:f}
    \
    '''
    print(about_the_train.format(learning_rate,iteration_count,cost_reduce_threshold))
    
    # start training
    cost_last = 0
    for i in range(iteration_count):
        grad_w,grad_b = gradient_caculate(x_train,y_train,w,b)
        w = w - learning_rate*grad_w
        b = b - learning_rate*grad_b
        cost_current = cost_function(w,b,x_train,y_train)
        if i >= iteration_count/2 and cost_last - cost_current<= cost_reduce_threshold:
            print("iteration: {:5d},cost_current:{:f},cost_last:{:f},cost reduce:{:f}".format( i+1,cost_current,cost_last,cost_last-cost_current))
            break
        if (i+1)%(iteration_count/10) == 0:
            print("iteration: {:5d},cost_current:{:f},cost_last:{:f},cost reduce:{:f}".format( i+1,cost_current,cost_last,cost_last-cost_current))
        cost_last = cost_current
    
    print("after the training, parameter w = {:f} and b = {:f}".format(w,b))
    
    for i in range(m):
        y = function_f(x_train[i],w,b)
        p  = 0
        if y>= 0.5: p  = 1
        print("sample: x[{:d}]:{:d},y[{:d}]:{:d}; the prediction is {:d} with probability:{:4f}".format(i,x_train[i],i,y_train[i],p,y))

    使用NumPy向量化 ssnn_ant_np.py
    """
    super simple neural networks(using numpy,snn.py not using numpy)
      * only one neuron in only the one hidden layer
      * input x is scalar (0-dimension)
      * using logistic function as the activation function
    
    input layer:
        x: scalar
    parameters:
        w: scalar
        b: scalar
    output:
        y \in [0,1] or  p \in {0,1}
    structure:
    
       x ->   w*x + b   ->   logistic function  -> output
            -----------      -----------------
                 |                    |
                 V                    V
             one neuron     activation function
    
    about it:
        it's a simple project for human learning how machine learning
        by orczhou.com
    """
    import numpy as np
    import math
    
    # function_f:
    # x   : scalar or ndarray
    # w   : scalar
    # b   : scalar
    def function_f(x,w,b):
        return 1/(1+np.exp(-(x*w+b)))
    
    # initial w,b
    w,b = (0,0)
    
    # samples
    x_train = np.array([0,1,2,3,4,5])
    y_train = np.array([0,0,0,0,0,1])
    #y_train = np.array([0,0,0,1,1,1])
    
    # m for sample counts
    m = x_train.shape[0]
    
    iteration_count = 50000
    learning_rate   = 0.01
    cost_reduce_threshold = 0.000001
    
    # Gradient caculate
    # w_p: current w
    # b_p: current b
    def gradient_caculate(w_p,b_p):
        gradient_w = np.sum((function_f(x_train,w_p,b_p) - y_train)*x_train)
        gradient_b = np.sum(function_f(x_train,w_p,b_p) - y_train)
        return gradient_w,gradient_b
    
    def cost_function(w_p,b_p,x_p,y_p):
        hat_y = function_f(x_p,w_p,b_p)
        c = np.sum(-y_p*np.log(hat_y) - (1-y_p)*np.log(1-hat_y))
        return c/m
    
    about_the_train = '''\
    try to train the model with:
      learning rate: {:f}
      max iteration : {:d}
      cost reduction threshold: {:f}
    \
    '''
    print(about_the_train.format(learning_rate,iteration_count,cost_reduce_threshold))
    
    # start training
    cost_last = 0
    for i in range(iteration_count):
        grad_w,grad_b = gradient_caculate(w,b)
        w = w - learning_rate*grad_w
        b = b - learning_rate*grad_b
        cost_current = cost_function(w,b,x_train,y_train)
        if i >= iteration_count/2 and cost_last - cost_current<= cost_reduce_threshold:
            print("iteration: {:5d},cost_current:{:f},cost_last:{:f},cost reduce:{:f}".format( i+1,cost_current,cost_last,cost_last-cost_current))
            break
        if (i+1)%(iteration_count/10) == 0:
            print("iteration: {:5d},cost_current:{:f},cost_last:{:f},cost reduce:{:f}".format( i+1,cost_current,cost_last,cost_last-cost_current))
        cost_last = cost_current
    
    print("after the training, parameter w = {:f} and b = {:f}".format(w,b))
    
    for i in range(m):
        y = function_f(x_train[i],w,b)
        p  = 0
        if y>= 0.5: p  = 1
        print("sample: x[{:d}]:{:d},y[{:d}]:{:d}; the prediction is {:d} with probability:{:4f}".format(i,x_train[i],i,y_train[i],p,y))

    使用TensorFlow实现该功能

    这里也是使用 TensorFlow 对上述问题中的数据进行训练并预测。详细代码和 TensorFlow 输出参考小结“TensorFlow 代码”和“TensorFlow的输出”。

    这里对其训练结果做简要的分析。在输出中,可以看到训练后的参数分别是:\( w = 1.374991 \quad b = -5.9958787 \),那么对应的预测表达式为:

    $$ \frac{1}{1+e^{-(w*x+b)}} $$

    代入 \( x = 1 \),其计算结果为:\( np.float64(0.009748092866213252) \),这与 TensorFlow 输出的 \( [0.00974809] \) 是一致的,这也验证了训练程序其实现与理解的事完全一致的。

    TensorFlow 代码 ssnn_ant_tf.py
    import tensorflow as tf
    import numpy as np
    
    tf.random.set_seed(1)
    X_train = np.array([[1], [2], [3], [4], [5],[6]], dtype=float)
    y_train = np.array([[0], [0], [0], [0], [1],[1]], dtype=float)
    
    model = tf.keras.Sequential([
        tf.keras.layers.Input(shape=(1,)),
        tf.keras.layers.Dense(units=1, activation='sigmoid')
    ])
    
    # model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
    model.compile(optimizer=tf.keras.optimizers.SGD(learning_rate=0.1), loss='binary_crossentropy', metrics=['accuracy'])
    
    model.fit(X_train, y_train, epochs=1000, verbose=0)
    model.summary()
    
    model.evaluate(X_train,  y_train, verbose=2)
    
    predictions = model.predict(X_train)
    print("Predictions:", predictions)
    
    for layer in model.layers:
        weights, biases = layer.get_weights()
        print("weights::", weights)
        print("biases:", biases)
    TensorFlow的输出
    Model: "sequential"
    ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓
    ┃ Layer (type)                         ┃ Output Shape                ┃         Param # ┃
    ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩
    │ dense (Dense)                        │ (None, 1)                   │               2 │
    └──────────────────────────────────────┴─────────────────────────────┴─────────────────┘
     Total params: 4 (20.00 B)
     Trainable params: 2 (8.00 B)
     Non-trainable params: 0 (0.00 B)
     Optimizer params: 2 (12.00 B)
    1/1 - 0s - 32ms/step - accuracy: 1.0000 - loss: 0.1856
    1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step
    Predictions: 
     [[0.00974809]
     [0.03747462]
     [0.13343701]
     [0.37850127]
     [0.7066308 ]
     [0.90500087]]
    weights:: [[1.374991]]
    biases: [-5.9958787]

    补充说明

    一个一般的前馈神经网络(FNN)通常至少需要一个隐藏层,并且在隐藏层有多个神经元。一个具有多个神经元的多层网络的结构和训练,其复杂度会高很多,后续会再做介绍。本文实现代码虽然只有99行代码,去掉注释只有70行代码,但麻雀虽小、五脏俱全,包含了梯度下降的实现、链式法则的应用等,如果理解了该示例,则可以很好帮助开发者打好基础,以便更好的理解更为复杂的神经网络的训练。

    此外,在计算中省略了一些求导计算,其中略微有一些复杂度的是 \( \frac{\partial L}{\partial \hat{y}} * \frac{\partial \hat{y}}{\partial z} \),感兴趣的可以自行补全。

  • 梯度下降法(或者其改进算法)是机器学习的基础算法之一。在了解梯度下降算法的过程中,会经常看到一句话:“梯度是函数在某一点变化率最大的方向”。本文从较为严格数学证明的角度说明为什么是这样。理解这个证明过程,可以很好的理解梯度下降算法,及其优化算法或者优化方向。

    本文主要考虑二元函数场景,即\( z=f(x,y) \)。原因是一元函数场景过于简单,不具有代表性,另外,二元场景向多元场景推广也还比较好理解。

    偏导数

    偏导数的定义比较好理解,即固定一个变量(当做常数),对另一个变量求导,记作:

    $$ \frac{\partial z}{\partial x} \; , \; \frac{\partial z}{\partial y} $$

    梯度向量

    由各个偏导数组成的向量,就叫梯度向量,通常记作:\( \nabla \),有:

    $$ \nabla f = (\frac{\partial z}{\partial x} , \frac{\partial z}{\partial y} ) $$

    多元/多维场景,则常记作:

    $$ \nabla f = (\frac{\partial f}{\partial x_1} , \frac{\partial f}{\partial x_2} … , \frac{\partial f}{\partial x_n} ) $$

    方向导数

    多元函数没有简单的“导数”的概念。但为了研究多元函数在某点的变化率,我们可以考虑“方向导数”。

    具体的,考虑函数 \( z = f(x,y) \),该函数定义域为\( \mathbb{R}^2 \),其方向向量是 $$ \{ u,v | u^2 +v^2 = 1 \} $$,取其中的一个方向 \( l = (u_0,v_0) \),并假设该方向与\( x \)轴正方向夹角为\( \theta \)。

    那么,函数\( z = f(x,y) \)在点\( (x_0,y_0) \)处,在方向 \( l = (u_0,v_0) \)的导数记作

    $$ \frac{\partial z}{\partial l} |_{(x_0,y_0)} $$

    直观理解方向导数

    图1是一个非常清晰的关于方向导数的图例。绿色曲面即为 \( z = f(x,y) \),在点\( A^\prime \)上考虑方向为\( \vec{h}\)的方向导数。过点\( A^\prime \)与方向\( \vec{h}\),与\( z \)轴平行,存在一个平面,即图1中的半透明的平面,该平面与 \( z = f(x,y) \)相交与一条曲线,即图1中的黄色曲线。

    那么,该方向导数,即为在该黄色曲线上,\( A^\prime \)位置的导数。这就是关于方向导数的直观理解。

    所以,偏导数\( \frac{\partial z}{\partial x} \; , \; \frac{\partial z}{\partial y} \)可以理解为在\( (1,0) \)和\( (0,1) \)这两个方向上的方向导数。

    图1:来自Wikipedia: Directional derivative

    与一般的导数定义类似的,可以定义方向导数:

    $$ \frac{\partial z}{\partial l} |_{(x_0,y_0)} = \lim\limits_{P \to P_0} = \frac{f(P) – f(P_0)}{||P-P_0||} = \lim\limits_{\rho \to 0} \frac{\Delta z}{ \rho } $$

    图2:\( P \) 点在\( (u,v) \)方向逼近\( P_0 \)

    可以到如下结论(详细证明参考后续小节“方向导数的计算与证明”),如果方向\( l = (u_0,v_0) \)与 \( x \)轴的夹角是\( \theta \),那么\( z = f(x,y) \)在点\( (x_0,y_0) \)处,在方向 \( l = (u_0,v_0) \)的导数取值如下:

    $$ \frac{\partial z}{\partial l} |_{(x_0,y_0)} = \frac{\partial z}{\partial x} |_{(x_0,y_0)} cos(\theta) + \frac{\partial z}{\partial y} |_{(x_0,y_0)} sin(\theta) \tag{1} $$

    根据柯西不等式,我们有如下结论:

    $$ \frac{\partial z}{\partial l} |_{(x_0,y_0)} = \frac{\partial z}{\partial x} |_{(x_0,y_0)} cos(\theta) + \frac{\partial z}{\partial y} |_{(x_0,y_0)} sin(\theta)
    \\
    \le \sqrt{ ((\frac{\partial z}{\partial x} |_{(x_0,y_0)})^2 + (\frac{\partial z}{\partial y} |_{(x_0,y_0)})^2)(sin^2(\theta)+cos^2(\theta)) }
    \\
    = \sqrt{ (\frac{\partial z}{\partial x} |_{(x_0,y_0)})^2 + (\frac{\partial z}{\partial y} |_{(x_0,y_0)})^2 }
    $$

    上面表示的极值 \( \sqrt{ (\frac{\partial z}{\partial x} |_{(x_0,y_0)})^2 + (\frac{\partial z}{\partial y} |_{(x_0,y_0)})^2 } \) 正是偏导数向量的“范数”(长度),根据柯西不等式取最大值的条件也有:

    $$
    \frac{cos(\theta)}{\frac{\partial z}{\partial x}} = \frac{sin(\theta)}{\frac{\partial z}{\partial y}}
    \\
    tan(\theta) = \frac{\frac{\partial z}{\partial y} } { \frac{\partial z}{\partial x} } = \frac{\Delta y}{\Delta x}
    $$

    所以,即,即当方向恰好为偏导数向量时,方向导数取最大值。也就是,我们经常会说的,会看到的,“偏导数向量是所有方向中最为陡峭的方向”或者说“梯度是函数在某一点变化率最大的方向”。

    方向导数的计算与证明

    在前面,我们是直接给出了如下的结论的:

    $$ \frac{\partial z}{\partial l} |_{(x_0,y_0)} = \frac{\partial z}{\partial x} |_{(x_0,y_0)} sin(\theta) + \frac{\partial z}{\partial y} |_{(x_0,y_0)} cos(\theta)$$

    这个结论的获得,是需要有一些比较复杂的计算或者说证明的。这里,其主要证明步骤/方法之一,如下:

    \( \frac{\partial z}{\partial l} |_{(x_0,y_0)} = \lim\limits_{P->P_0}\frac{f(P)-f(P_0)}{|P-P_0|} = \lim\limits_{P->P_0}\frac{f(x_0+\Delta{x},y_0+\Delta{y})-f(x_0,y_0)}{\sqrt{\Delta{x}^2+\Delta{y}^2}}
    \)

    由拉格朗日中值定理:存在\( \alpha \; \beta \),使得下式成立,且 \( 0 \le \alpha \le 1 \; and \; 0 \le \beta \le 1 \):

    \(
    f(x_0+\Delta{x},y_0+\Delta{y})-f(x_0,y_0)
    \\
    = [f(x_0+\Delta{x},y_0+\Delta{y}) – f(x_0,y_0+\Delta{y})] + [f(x_0,y_0+\Delta{y}) -f(x_0,y_0)]
    \\
    = f_x'(x_0 + \alpha\Delta{x} ,y_0+\Delta{y})\Delta{x} + f_y'(x_0, y_0 + \beta\Delta{y} )\Delta{y}
    \)

    容易有,这几个条件是等价的: \( P \to P_0 \)、\( \Delta{x} \to 0 \, and \, \Delta{y} \to 0 \) 、\( \sqrt{\Delta{x}^2+\Delta{y}^2} \to 0 \)

    考虑\( \frac{\partial z}{\partial x} \)在\( (x_0,y_0)\)处连续(这是一个条件),则有: $$ \lim\limits_{\Delta{x} \to 0 \\ \Delta {y} \to 0 }f_x'(x_0 + \alpha\Delta{x} ,y_0+\Delta{y}) = f_x'(x_0,y_0) $$

    故:

    $$
    \begin{align}
    \frac{\partial z}{\partial l} |_{(x_0,y_0)} & = \lim\limits_{P->P_0}\frac{f(P)-f(P_0)}{|P-P_0|}
    \\
    & = \lim\limits_{P->P_0}\frac{f(x_0+\Delta{x},y_0+\Delta{y})-f(x_0,y_0)}{\sqrt{\Delta{x}^2+\Delta{y}^2}}
    \\
    & =\lim\limits_{P->P_0}\frac{f_x'(x_0+\alpha\Delta{x},y_0+\Delta{y})\Delta{x} + f_y'(x_0,y_0+\Delta{y})\Delta{y}}{\sqrt{\Delta{x}^2+\Delta{y}^2}}
    \\
    & =\lim\limits_{P->P_0}\frac{f_x'(x_0+\alpha\Delta{x},y_0+\Delta{y})\Delta{x}}{\sqrt{\Delta{x}^2+\Delta{y}^2}} + \frac{f_y'(x_0,y_0+\Delta{y})\Delta{y}}{\sqrt{\Delta{x}^2+\Delta{y}^2}}
    \end{align}
    $$

    根据上面的图2,容易有:

    $$
    \frac{\Delta{x}}{\sqrt{\Delta{x}^2+\Delta{y}^2}} = cos(\theta) \quad \frac{\Delta{y}}{\sqrt{\Delta{x}^2+\Delta{y}^2}} = sin(\theta)
    $$

    所以:

    \( =\lim\limits_{P->P_0}\frac{f_x'(x_0+\alpha\Delta{x},y_0+\Delta{y})\Delta{x}}{\sqrt{\Delta{x}^2+\Delta{y}^2}} + \frac{f_y'(x_0,y_0+\Delta{y})\Delta{y}}{\sqrt{\Delta{x}^2+\Delta{y}^2}}
    \\
    =f_x'(x_0,y_0)cos(\theta) + f_y'(x_0,y_0)sin(\theta)
    \\
    \)

    好了,这就证明完成了。

    关于上述证明

    上述证明,在一般的《数学分析》教程的“多元函数微分”相关章节都会有,或者会有类似的问题证明。过程还是比较巧妙的,先是“无中生有”新增了一个项(\( f(x_0,y_0+\Delta{y}) \)),分别构造了关于 \( x \)和\( y \)的偏导数,然后使用了“中值定理”,将差值变成,导数和微分变量的积(准确的说,还要加上一个关于\( \rho \)的高阶无穷小)。

    向量形式化表达

    使用向量形式化表达,看起来会简洁很多。对于方向向量(这也是一个单位向量) \( \mathbf{l} = (u,v)\),函数\( f \)的偏导数向量记为\( \nabla f = (\frac{\partial z}{\partial x} , \frac{\partial z}{\partial y} ) \) ,那么方向导数为 \( D_{\mathbf{l}}f(P_0) = \nabla f \cdot \mathbf{l} \) ,这与上面表达式的意义是相同的。

    根据点击的性质,我们有:

    \( D_{\mathbf{l}}f(P_0) = \nabla f \cdot \mathbf{l} = ||\nabla f|| ||\mathbf{l} || cos\theta = ||\nabla f|| cos\theta \)

    从这里,更容易看出,方向向量与梯度向量相同时,方向导数取最大值,最大值即为梯度向量的模。

    多维场景扩展

    在很多的材料中,在前面的表达式中,经常会看到的是 \( cos(\alpha) \; cos(\beta) \),而不是本文中的 \( sin(\theta) \; cos(\theta) \)。这里的 \( \alpha \)是方向向量与x轴正方向的夹角, \( \beta \)是方向向量与y轴正方向的夹角;在定义域 \( \mathbb{R}^2 \)上有:\( \alpha + \beta = 90^{\circ} \),即有 \( cos^2\alpha + cos^2\beta = 1 \)。

    这种写法有着更好的扩展性,当在更多元的情况下,例如三元场景下,即 \( z = f(x_1,x_2,x_3) \),方向向量与 x,y,z轴的夹角分别是:\( \alpha \; \beta \; \gamma \),则有: \( cos^2\alpha + cos^2\beta + cos^2 \gamma = 1 \)。

    任意维度,也有类似的结论,并且应用柯西不等式时,上述结论也是类似的。

    说明:直觉

    本文内容需要或者可以建立如下的“直觉”:

    • 在一维空间(即\( \mathbb{R}\)上的函数,在某一点上的一阶导数的符号(正/负),可以代表在该方向上,函数的趋势是增长还是下降,“正号”,则是增长;“负号”,则是下降。
    • 在一维空间(即\( \mathbb{R}\)上的函数,在某一点上的一阶导数的绝对值大小,即为其“陡峭程度”(更多的时候理解为,变化率大小)

    上述两个结论,基本上认为是显然的。下面扩展到多维场景,也几乎是显然的:

    • 在高维空间/多维变量(即\( \mathbb{R}^n\)时,在某一点的任意方向上,都有导数,称为方向导数,该方向导数的符号(正/负),可以代表在该方向上,函数的趋势是增长还是下降,“正号”,则是增长;“负号”,则是下降。
    • 在高维空间/多维变量(即\( \mathbb{R}^n\)时,在某一点的任意方向上,都有导数,该导数的绝对值大小,即为其“陡峭程度”(更多的时候理解为,变化率大小)
    • 更进一步的,也就是本文中的一个结论:高维空间/多维变量(即\( \mathbb{R}^n\)时,函数的所有的方向导数,在偏导数向量方向上,取值最大,即是最为“陡峭”的方向。

    所以,最后

    所以,这就是为什么梯度下降算法中,总是倾向于选择偏导数向量方向进行下一次迭代。

    在本科毕业后,最后留了几本书:《数学分析》(上下册)、概率论,一直到研究生毕业、再到工作都一直带着,还从北京邮寄到了杭州。本想只是做个纪念的,没想到竟然还能用上…

  • 如果手动安装的话,那还比较简单,基本上点点就可以了。但是如果使用Terraform自动化安装AlmaLinux,则比想象的要复杂。这个复杂度对于所以云市场(MarketPlace)的资源都有类似的问题:需要先在Terraform脚本中同意相关协议才能够安装,这是复杂度的主要原因。

    这里给出示例,供参考。

    获取AlmaLinux的NameOCID

    OCID是Oracle Cloud上所有云资源的唯一标识符。每一个OS镜像也都有一个OCID。在官网页面上,也只是列出小部分Oracle镜像,以及对应的OCID:参考All Image Families。但是,对于大量来自其云市场(MarketPlace)的镜像,这里是查不到的,即便在云市场能够查到,也并没有给出OCID

    (more…)
  • 在使用梯度下降法进行回归时,需要频繁的进行偏导数的计算。在很多的相关介绍中会展示使用计算图进行偏导数的计算。这里简述对该方法的一些理解。


    概述

    • 计算图求导,可以理解为是对求导的链式法则的图表示
    • 在计算图中,在一个单向路径上的算子,求导时,将各个导数相乘即可
    • 在计算图中,在一个单向路径上,上一个节点的输出,是下一个节点的输入;函数关系上,就是 \( f(g(x)) \),即\( g(x) \)的输出是,\( f(x) \)的输入
    • 一个节点的两个入度(分支),求导时,将各个导数相乘即可
    • 多元函数求偏导时,只需要关注其偏导的变量即可

    链式法则的典型形式

    这里对求导的链式法则的典型形式做一个简单的回顾。

    在对复杂的表达式求导/微分时,有时候看起来会很复杂。如果能够灵活的使用链式法则可以巧妙将复杂函数的求导转换为简单函数的求导。

    法则1:

    $$
    f(x) = g(h(x))
    \\
    f'(x) = \frac{\partial f}{\partial x} = \frac{\partial g}{\partial h} \frac{\partial h}{\partial x}
    $$

    例如,使用该法则可以很简单对如下函数求导:

    $$
    f(x) = e^{(x^2)}
    \\
    g(h) = e^h \, h(x) = x^2
    \\
    f'(x) = \frac{\partial g}{\partial h} \frac{\partial h}{\partial x} = e^h * 2 * x = 2x*e^h = 2xe^{x^2}
    $$

    如果使用计算图的方式表达如上的求导,如下:

    $$
    f(x) = f(g(x))
    \\
    \frac{\partial f}{\partial x} = \frac{\partial f}{\partial g}\frac{\partial g}{\partial h}
    $$

    所以:在计算图中,在一个单向路径上的算子,求导时,将各个导数相乘即可。

    (more…)
  • 这是一个系列文章,旨在了解如何使用Flex/Lex和Yacc/Bison进行词法和语法解析。在前面,已经完成了使用Lex/flex做基础的词法解析实现一个简单的计算器flex/bison系列3:更复杂的一个编译程序实现(上)。在上篇中,已经完成语法规则、主要的数据结构设计。这里就继续完成程序,最后编译测试。

    回顾

    这个系列我们需要通过flex/bison实现一个编译程序,能够实现一种简单的程序语言,这个程序语言包含了:基础运算、变量与赋值、表达式计算、if语句、while语句、print语句等。例如,使用该程序语言,我们可以实现如下程序:

    i = 1;
    a = 0;
    while ( a < 100 ) {
      i = i + 1;
      a = a + i;
    }
    print i;

    该程序解决的问题是:在自然数序列(1、2、4…)中,前面多少个自然数的和首次大于100。你可以使用上面定义的语言,编写自己的程序。

    好了,接着前面三篇的内容,我们继续完成该语言的编译程序。

    主要的函数实现

    build_node函数
    t_node* build_node(enum NODETYPE nt,t_node* left,t_node* right, int i){
        debug_print(__FILE__,__LINE__,__func__,"");
        t_node *t_n;
        t_n = NULL;
        t_n = (t_node *)malloc(sizeof(t_node));
        if (t_n == NULL){
            printf("Out of Memory\n");
            exit(1);
        }
        t_n->nt = nt;
        t_n->left = left;
        t_n->right = right;
        t_n->i = i;
        return t_n;
    }
    exec_node函数
    int exec_node(t_node *n){
        if( n == NULL ) return 0;
        debug_print(__FILE__,__LINE__,__func__,"enter exec_node");
    
        switch(n->nt){
            case NT_INTEGER:
        	    debug_print(__FILE__,__LINE__,__func__,"NT_INTEGER node");
    	    break;
            case NT_VAR_NAME:
        	    debug_print(__FILE__,__LINE__,__func__,"NT_VAR_NAME node");
    	    break;
            case NT_O_ADD:
                exec_node(n->left);
                exec_node(n->right);
                n->i = get_node_ret(n->left) + get_node_ret(n->right);
        	    debug_print(__FILE__,__LINE__,__func__,"NT_O_ADD node");
    	    break;
            case NT_O_MINUS:
                exec_node(n->left);
                exec_node(n->right);
                n->i = get_node_ret(n->left) - get_node_ret(n->right);
        	    debug_print(__FILE__,__LINE__,__func__,"NT_O_MINUS node");
                break;
            case NT_O_MULTIPLY:
        	    debug_print(__FILE__,__LINE__,__func__,"NT_O_MULTIPLY node");
                exec_node(n->left);
                exec_node(n->right);
                n->i = get_node_ret(n->left) * get_node_ret(n->right);
                break;
            case NT_BOOL_EXPR_GT:
        	    debug_print(__FILE__,__LINE__,__func__,"NT_BOOL_EXPR_GT node");
                n->i = 0;
                exec_node(n->left);
                exec_node(n->right);
                if (get_node_ret(n->left) > get_node_ret(n->right) )
                { n->i = 1 ; }
                break;
            case NT_BOOL_EXPR_LT:
        	    debug_print(__FILE__,__LINE__,__func__,"NT_BOOL_EXPR_LT node");
                n->i = 0;
                exec_node(n->left);
                exec_node(n->right);
                if (get_node_ret(n->left) < get_node_ret(n->right) )
                { n->i = 1 ; }
                break;
            case NT_IF:
        	    debug_print(__FILE__,__LINE__,__func__,"NT_IF node");
                exec_node(n->left);
                if (get_node_ret(n->left)){
                    exec_node(n->right);
                }
                break;
            case NT_WHILE:
        	    debug_print(__FILE__,__LINE__,__func__,"NT_WHILE node");
                exec_node(n->left);
                while( get_node_ret(n->left)  ){
                    exec_node(n->right);
                    exec_node(n->left);
                }
                break;
            case NT_PRINT:
                debug_print(__FILE__,__LINE__,__func__,"NT_PRINT node");
                exec_node(n->left);
                printf("print '%d'",get_node_ret(n->left));
                break;
            case NT_ASSIGNMENT:
        	    debug_print(__FILE__,__LINE__,__func__,"NT_ASSIGNMENT node");
                exec_node(n->left);
                exec_node(n->right);
                var[n->left->i - 'a'] = get_node_ret(n->right);
                break;
            case NT_STATEMENT_BLOCK:
        	    debug_print(__FILE__,__LINE__,__func__,"NT_STATEMENT_BLOCK node");
                exec_node(n->left);
                exec_node(n->right);
                break;
            case NT_PROGRAM:
        	    debug_print(__FILE__,__LINE__,__func__,"NT_PROGRAM node");
                break;
        }
    
        return 0;
    }
    内存释放
    int free_node(t_node *n){
        if( n != NULL ) {
            free_node(n->left);
            free_node(n->right);
        }
        free(n);
        return 0;
    }
    工具函数debug_print
    void debug_print(const char *fname, int lineno, const char *fxname, const char *debug_info){
        #ifdef cal_DEBUG
        printf("\n debug: enter at line %d in %s,function: %s info: %s\n",
            lineno,
            fname,
            fxname,
    		debug_info
            );
    	#endif
    }

    补充语法文件的Action部分

    %%
    program:  statement_block
                {
                    exec_node($1);
                    free_node($1);
                    printf("\n job done! \n");
                }
    ;
    
    statement_block: %empty
                {
                    $$ = build_node(
                            NT_STATEMENT_BLOCK,
                            NULL,
                            NULL,
                            NULL,
                            0
                            );
                    debug_print(__FILE__,__LINE__,__func__,"");
                }
    
            | statement_block statement
                { $$ = build_node(
                        NT_STATEMENT_BLOCK,
                        $1,
                        $2,
                        NULL,
                        0
                        );
    
                    debug_print(__FILE__,__LINE__,__func__,"");
                }
    ;
    
    statement: assignment { $$ = $1; }
            | print_func  { $$ = $1; }
            | if_block    { $$ = $1; }
            | while_block { $$ = $1; }
    ;
    
    if_block: IF '(' bool_expr ')' '{' statement_block '}'
                {
                    $$ = build_node(
                            NT_IF,
                            $3,
                            $6,
                            NULL,
                            0
                            );
                }
    
    while_block: WHILE '('  bool_expr ')' '{' statement_block '}'
                {
                    $$ = build_node(
                            NT_WHILE,
                            $3,
                            $6,
                            NULL,
                            0
                            );
                }
    
    assignment: VAR_NAME '=' expression ';' { $$ = build_node(
                                                        NT_ASSIGNMENT,
                                                        build_node(NT_VAR_NAME,NULL,NULL,NULL,(int)$1),
                                                        $3,
                                                        NULL,
                                                        0);
                                            }
    
    print_func : PRINT expression ';'   {  $$ = build_node(NT_PRINT,$2,NULL,NULL,0); }
    
    bool_expr: expression GT expression {  $$ = build_node(NT_BOOL_EXPR_GT,$1,$3,NULL,0);}
            |  expression LT expression {  $$ = build_node(NT_BOOL_EXPR_LT,$1,$3,NULL,0);}
    
    expression: INTEGER { $$ = build_node(NT_INTEGER,NULL,NULL,NULL,$1); }
            | VAR_NAME  { $$ = build_node(NT_VAR_NAME,NULL,NULL,NULL,(int)$1); }
            | expression O_ADD expression {  $$ = build_node(NT_O_ADD,$1,$3,NULL,0);}
            | expression O_MINUS expression  {  $$ = build_node(NT_O_MINUS,$1,$3,NULL,0);}
            | expression O_MULTIPLY expression {  $$ = build_node(NT_O_MULTIPLY,$1,$3,NULL,0);}
    
    
    %%

    cal.header.h

    enum NODETYPE{
        NT_STATEMENT,
        NT_IF,
        NT_WHILE,
        NT_PROGRAM,
        NT_STATEMENT_BLOCK,
        NT_O_ADD,
        NT_O_MINUS,
        NT_O_MULTIPLY,
        NT_INTEGER,
        NT_VAR_NAME,
        NT_BOOL_EXPR_GT,
        NT_BOOL_EXPR_LT,
        NT_PRINT,
        NT_ASSIGNMENT
    };
    
    typedef struct t_node{
        enum NODETYPE nt;
        struct t_node* left;
        struct t_node* right;
        struct t_node* rrnode;
        int i;  // for NT_INTEGER NT_VAR_NAME node
    }t_node;

    有了这些信息,就可以使用NODETYPE来构建,解析树了。在每次解析到对应节点或进行Reduction时,我们在语法文件的Action部分就可以调用一个build_node函数来构建对应的节点。我们可以看看如下的程序的解析树结构:

    i = 1 ;
    a = 0 ;
    while ( a < 100 ) {
        a = a + i;
        i = i + 1;
    }
    print i ;

    这个程序,可以找到,在自然数级数中,到第几项的时候,其和就超过了100。

    完整的代码

    最后是程序实现的部分,包括

    • build_node
    • execute_node
    • free_node
    • get_node_ret

    cal.l lex文件
    cat cal.l
    
    %{
        #include "cal.tab.h"
    %}
    %option noyywrap
    %%
    [[:digit:]]+ {
        yylval.a = atoi(yytext);
        return INTEGER;
    }
    
    [a-z] {
        yylval.c = yytext[0];
        return VAR_NAME;
    }
    
    "+" { return O_ADD;};
    "-" { return O_MINUS;};
    "*" { return O_MULTIPLY;};
    
    "while"  {return WHILE;}
    "if"  {return IF;}
    "print"  {return PRINT;}
    
    ">" {return GT;}
    "<" {return LT;}
    
    [();={}]  {return yylval.c = *yytext;}
    
    %%
    cal.header.h 头文件/数据结构定义
    cat cal.header.h
    
    enum NODETYPE{
        NT_STATEMENT,
        NT_IF,
        NT_WHILE,
        NT_PROGRAM,
        NT_STATEMENT_BLOCK,
        NT_O_ADD,
        NT_O_MINUS,
        NT_O_MULTIPLY,
        NT_INTEGER,
        NT_VAR_NAME,
        NT_BOOL_EXPR_GT,
        NT_BOOL_EXPR_LT,
        NT_PRINT,
        NT_ASSIGNMENT
    };
    
    typedef struct t_node{
        enum NODETYPE nt;
        struct t_node* left;
        struct t_node* right;
        struct t_node* rrnode;
        int i;  // for NT_INTEGER NT_VAR_NAME node
    }t_node;
    cal.y 语言语法文件
    cat cal.y
    %{
    
    #include <stdio.h>
    #include <stdlib.h>
    #include "cal.tab.h"
    #include "cal.header.h"
    
    // #define cal_DEBUG 1
    
    void debug_print(const char *fname, int lineno, const char *fxname, const char *debug_info){
        #ifdef cal_DEBUG
        printf("\n debug: enter at line %d in %s,function: %s info: %s\n",
            lineno,
            fname,
            fxname,
    		debug_info
            );
    	#endif
    }
    
    
    t_node* build_node(enum NODETYPE nt,t_node* left,t_node* right, t_node* r_right , int i){
        debug_print(__FILE__,__LINE__,__func__,"");
        t_node *t_n;
        t_n = NULL;
        t_n = (t_node *)malloc(sizeof(t_node));
        if (t_n == NULL){
            printf("Out of Memory\n");
            exit(1);
        }
    	t_n->nt = nt;
    	t_n->left = left;
    	t_n->right = right;
    	t_n->rrnode = r_right;
    	t_n->i = i;
        return t_n;
    }
    
    
    int var[26];
    
    int main (){
        int yydebug=1;
        yyparse();
        return 0;
    }
    
    void
    yyerror (char const *s)
    {
      fprintf (stderr, "something error: %s\n over", s);
    }
    
    
    int exec_node(t_node *n){
        if( n == NULL ) return 0;
        debug_print(__FILE__,__LINE__,__func__,"enter exec_node");
    
        switch(n->nt){
            case NT_INTEGER:
        		debug_print(__FILE__,__LINE__,__func__,"NT_INTEGER node");
    			break;
            case NT_VAR_NAME:
        		debug_print(__FILE__,__LINE__,__func__,"NT_VAR_NAME node");
    			break;
            case NT_O_ADD:
                exec_node(n->left);
                exec_node(n->right);
                n->i = get_node_ret(n->left) + get_node_ret(n->right);
        		debug_print(__FILE__,__LINE__,__func__,"NT_O_ADD node");
    			break;
            case NT_O_MINUS:
                exec_node(n->left);
                exec_node(n->right);
                n->i = get_node_ret(n->left) - get_node_ret(n->right);
        		debug_print(__FILE__,__LINE__,__func__,"NT_O_MINUS node");
                break;
            case NT_O_MULTIPLY:
        		debug_print(__FILE__,__LINE__,__func__,"NT_O_MULTIPLY node");
                exec_node(n->left);
                exec_node(n->right);
                n->i = get_node_ret(n->left) * get_node_ret(n->right);
                break;
            case NT_BOOL_EXPR_GT:
        		debug_print(__FILE__,__LINE__,__func__,"NT_BOOL_EXPR_GT node");
                n->i = 0;
                exec_node(n->left);
                exec_node(n->right);
                if (get_node_ret(n->left) > get_node_ret(n->right) )
                { n->i = 1 ; }
                break;
            case NT_BOOL_EXPR_LT:
        		debug_print(__FILE__,__LINE__,__func__,"NT_BOOL_EXPR_LT node");
                n->i = 0;
                exec_node(n->left);
                exec_node(n->right);
                if (get_node_ret(n->left) < get_node_ret(n->right) )
                { n->i = 1 ; }
                break;
            case NT_IF:
        		debug_print(__FILE__,__LINE__,__func__,"NT_IF node");
                exec_node(n->left);
                if (get_node_ret(n->left)){
                    exec_node(n->right);
                }
                break;
            case NT_WHILE:
        		debug_print(__FILE__,__LINE__,__func__,"NT_WHILE node");
                exec_node(n->left);
                while( get_node_ret(n->left)  ){
                    exec_node(n->right);
                    exec_node(n->left);
                }
                break;
            case NT_PRINT:
        		debug_print(__FILE__,__LINE__,__func__,"NT_PRINT node");
                exec_node(n->left);
                printf("print '%d'",get_node_ret(n->left));
                break;
            case NT_ASSIGNMENT:
        		debug_print(__FILE__,__LINE__,__func__,"NT_ASSIGNMENT node");
                exec_node(n->left);
                exec_node(n->right);
                var[n->left->i - 'a'] = get_node_ret(n->right);
                break;
            case NT_STATEMENT_BLOCK:
        		debug_print(__FILE__,__LINE__,__func__,"NT_STATEMENT_BLOCK node");
                exec_node(n->left);
                exec_node(n->right);
                break;
            case NT_PROGRAM:
        		debug_print(__FILE__,__LINE__,__func__,"NT_PROGRAM node");
                break;
        }
    
        return 0;
    }
    
    int get_node_ret(t_node *n){
        int r = n->i;
        switch(n->nt){
            case NT_VAR_NAME:
                r = var[n->i - 'a'];
                break;
        }
        return r;
    }
    
    int free_node(t_node *n){
        if(n != NULL){
            // printf("\n try to free memory of node %d \n",n->nt);
        }
        return 0;
    }
    
    
    %}
    
    %union {
        int a;  // for integer
        char c; // for var_name
        int int_bool; // for bool_expr
        struct t_node* t_n;
    }
    
    
    %type <t_n> expression bool_expr print_func assignment
    %type <t_n> while_block statement statement_block if_block
    
    %token <c> VAR_NAME
    %token <a> INTEGER
    
    %token O_ADD O_MINUS O_MULTIPLY
    
    %token GT LT
    
    %token WHILE IF
    %token PRINT
    
    %left O_ADD O_MINUS
    %left O_MULTIPLY
    
    %%
    program:  statement_block
                {
                    exec_node($1);
                    free_node($1);
                    printf("\n job done! \n");
                }
    ;
    
    statement_block: %empty
                {
                    $$ = build_node(
                            NT_STATEMENT_BLOCK,
                            NULL,
                            NULL,
                            NULL,
                            0
                            );
                    debug_print(__FILE__,__LINE__,__func__,"");
                }
    
            | statement_block statement
                { $$ = build_node(
                        NT_STATEMENT_BLOCK,
                        $1,
                        $2,
                        NULL,
                        0
                        );
    
                    debug_print(__FILE__,__LINE__,__func__,"");
                }
    ;
    
    statement: assignment { $$ = $1; }
            | print_func  { $$ = $1; }
            | if_block    { $$ = $1; }
            | while_block { $$ = $1; }
    ;
    
    if_block: IF '(' bool_expr ')' '{' statement_block '}'
                {
                    $$ = build_node(
                            NT_IF,
                            $3,
                            $6,
                            NULL,
                            0
                            );
                }
    
    while_block: WHILE '('  bool_expr ')' '{' statement_block '}'
                {
                    $$ = build_node(
                            NT_WHILE,
                            $3,
                            $6,
                            NULL,
                            0
                            );
                }
    
    assignment: VAR_NAME '=' expression ';' { $$ = build_node(
                                                        NT_ASSIGNMENT,
                                                        build_node(NT_VAR_NAME,NULL,NULL,NULL,(int)$1),
                                                        $3,
                                                        NULL,
                                                        0);
                                            }
    
    print_func : PRINT expression ';'   {  $$ = build_node(NT_PRINT,$2,NULL,NULL,0); }
    
    bool_expr: expression GT expression {  $$ = build_node(NT_BOOL_EXPR_GT,$1,$3,NULL,0);}
            |  expression LT expression {  $$ = build_node(NT_BOOL_EXPR_LT,$1,$3,NULL,0);}
    
    expression: INTEGER { $$ = build_node(NT_INTEGER,NULL,NULL,NULL,$1); }
            | VAR_NAME  { $$ = build_node(NT_VAR_NAME,NULL,NULL,NULL,(int)$1); }
            | expression O_ADD expression {  $$ = build_node(NT_O_ADD,$1,$3,NULL,0);}
            | expression O_MINUS expression  {  $$ = build_node(NT_O_MINUS,$1,$3,NULL,0);}
            | expression O_MULTIPLY expression {  $$ = build_node(NT_O_MULTIPLY,$1,$3,NULL,0);}
    
    
    %%

    编译与执行

    lex cal.l && \
    bison -d cal.y && \
    gcc cal.tab.c lex.yy.c -o a.out && \
    ./a.out < p.f.txt

    最后,需要注意,该程序更注重的是测试与实现,所以在“内存释放”可能会存在一些泄露的问题。

  • 个人的一些脚本和代码,经常会分散在不同的地方,管理起来并不方便,例如给WordPress编写的Plugin、测试MySQL时使用的一些脚本等,所以打算全部使用GitHub管理起来。对于个人使用,GitHub提供了私人仓库以存储代码,可以较为方便的管理一些还没有公开的个人代码。

    建立个人Git和GitHub环境

    GitHub CLI是一个具体简单交互式操作的命令行,可以完成与GitHub相关的一些交互与操作。对应的软件包/命令是gh

    安装gh-cli

    参考:Installing gh on Linux and BSD。Amazon Linux 2上安装:

    sudo yum-config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.repo
    sudo yum install gh

    使用gh配置GitHub授权

    接着,就可以使用gh auth login命令来进行GitHub的认证了(gh cli manual)。这是一个简单的交互式命令,这里使用https+token的方式完成认证(也可以使用浏览器的方式辅助完成命令行认证):

    gh auth login
    ? What account do you want to log into? GitHub.com
    ? What is your preferred protocol for Git operations on this host? HTTPS
    ? Authenticate Git with your GitHub credentials? Yes
    ? How would you like to authenticate GitHub CLI? Paste an authentication token
    Tip: you can generate a Personal Access Token here https://github.com/settings/tokens
    The minimum required scopes are 'repo', 'read:org', 'workflow'.
    ? Paste your authentication token: *********************************************************************************************
    - gh config set -h github.com git_protocol https
    ✓ Configured git protocol
    ! Authentication credentials saved in plain text
    ✓ Logged in as orczhou

    关于Token的配置与获取,可以参考:GitHub->Settings->Developer Settings ,这里不再详述。注意,Token意味着分配的所有的仓库权限,必须妥善保管,否则可能会带来巨大的安全隐患。

    如果要登出的话,则可以简单的使用如下命令:

    gh auth logout

    在本地pull与push仓库

    • 首先,在git中配置本地身份(用户名与)
    git config --global user.name "orczhou"
    git config --global user.email "orczhou@orczhou"
    • 首先,新建一个本地模板,并使用git命令初始化
    mkdir  terraform && cd terraform
    git init
    • 配置远端(remote)分支;并拉取远端代码
    git remote add origin https://github.com/orczhou/cloud-mysql-benchmark.git
    git pull origin main

    向远端push代码

    这时,如果修改了仓库中的代码,则可以使用push命令向远端发起提交请求。

    修改、测试并本地提交代码:

    vi gcp_rds_ins/all_in_one/README.md
    git add gcp_rds_ins/all_in_one/README.md
    git commit -m "gcp readme updated"

    向远端push修改:

    git push -u origin main

    该操作会向远端仓库的main分支,提交代码。

    向main分之合并代码

    可以在GitHub仓库页面,对比并创建一个pull request。

    发起pr之后,代码仓库则可以进行merge操作,将代码合并到main分之。

    在新增远程代码库(origin)

    git remote add origin https://github.com/orczhou/testing-delete-repo-if-u-like.git

    将本地代码,提交到远程代码库(origin)的main分支:

    git push -u origin main

    上面的,-u origin main ,其中-u参数表示push的目标代码库-u | --set-upstream

    在现有仓库的main分之上开发

    经常需要做这个动作,常用的命令比较简单,这里记录如下:

    mkdir repo_bench && cd repo_bench
    git init
    git branch -M main
    git remote add origin https://...
    git pull origin main
    
    

    直接修改本地main中的代码并提交到源端:

    cat "..." > README.md
    git add README.md
    git commit -m "first commit" --dry-run
    git commit -m "first commit"
    git push -u origin main

    使用gitignore忽略文件

    在代码开发过程中,由于编译、运行等原因会产生很多的中间文件,而这些中间文件时无需提交到代码仓库的。这时候,需要使用gitignore来忽略这部分文件。详细完整的gitignore的使用可以参考man gitignore

    常用的gitignore是可以通过.gitignore文件来实现,即在代码的根目录中新建该文件,那么在代码处理时,就会根据根据该文件的规则进行忽略。例如Terraform脚本常用的gitignore文件可以参考:

    所以,一个Terraform脚本的.gitignore可以依次作参考:

    # Compiled files
    *.tfstate
    *.tfstate.backup
    *.tfstate.lock.info
    
    # Directories
    .terraform/
    .vagrant/
    
    # SSH Keys
    *.pem
    
    # Ignored Terraform files
    *gitignore*.tf

    master分支与main分支

    在搜索git/github的相关资料的时候,经常还会搜索到master分支作为主分支的资料或者仓库。在2020年的George Floyd的案件发生后,美国的Black_Lives_Matter运动达到了前所未有的高度,最终也影响到在计算机领域的master/slave 一词的使用。更多的参考:Renaming the default branch from master@GitHubWhy GitHub renamed its master branch to main@theserverside

    不过,git在本地默认还是使用默认的master分支,所以如果没有手动切换分支,则还是会经常“默认的”创建master分支。

    查看未提交的修改

    git面向的场景就是分布式、多任务的开发代码管理,其独特的”three tree“模型可以很巧妙的实现这些能力。这也给初学者带来了很多理解上的障碍。

    git diffgit diff HEAD

    如果,想要查看自上次commit以来的所有变更,则需要试用git diff HEAD命令,通常HEAD指向的是,最后一次commit时的位置。

    # diff between "working" and "staging"
    git diff
    # diff between "working" and "repository"
    git diff HEAD
    # diff between "staging" and "repository"
    git diff --cached

    同步远程更新

    个人代码仓库管理中,有时候会有这样的情况:直接在远程仓库中修改了一些文件,然后如何让本地和远程保持同步。考虑这样的场景:直接在GitHub上对README.md文件进行了编辑,那么本地代码仓库如何保持这个更新。

    当然,这样做,通常可能会很危险:可能会覆盖掉你本地所做的更改,但是基于上面的场景,所以,有时候会需要这么做。Stackoverflow上有几个相关的问题,非常详细的介绍了做法:

    这里的推荐做法是这样,如果本地仓库的修改确定不要了(通常这是很危险的):

    git pull

    如果本地仓库修改都还需要:

    git stash
    git pull 
    git stash pop

    还可以:

    • 先使用 git fetch更新origin/main
    • 然后使用git diff main origin/main查看本地与远程的差异
    • 最后使用git mergeorigin/main与本地合并,并保持在本地

    这样origin/main是最新的,且本地分支也是最新的了

    git fetch
    git diff main origin/main
    git merge

    参考链接