import numpy as np


class Tensor(object):

    def __init__(self, data,
                 autograd=False,
                 creators=None,
                 creation_op=None,
                 id=None):

        self.data = np.array(data)
        self.autograd = autograd
        self.grad = None
        if (id is None):
            self.id = np.random.randint(0, 100000)
        else:
            self.id = id

        self.creators = creators
        self.creation_op = creation_op
        self.children = {}

        if (creators is not None):
            for c in creators:
                if (self.id not in c.children):
                    c.children[self.id] = 1
                else:
                    c.children[self.id] += 1

    def all_children_grads_accounted_for(self):
        for id, cnt in self.children.items():
            if (cnt != 0):
                return False
        return True

    def backward(self, grad=None, grad_origin=None):
        if (self.autograd):

            if (grad is None):
                grad = Tensor(np.ones_like(self.data))

            if (grad_origin is not None):
                if (self.children[grad_origin.id] == 0):
                    raise Exception("cannot backprop more than once")
                else:
                    self.children[grad_origin.id] -= 1

            if (self.grad is None):
                self.grad = grad
            else:
                self.grad += grad

            # grads must not have grads of their own
            assert grad.autograd == False

            # only continue backpropping if there's something to
            # backprop into and if all gradients (from children)
            # are accounted for override waiting for children if
            # "backprop" was called on this variable directly
            if (self.creators is not None and
                    (self.all_children_grads_accounted_for() or
                     grad_origin is None)):

                if (self.creation_op == "add"):
                    self.creators[0].backward(self.grad, self)
                    self.creators[1].backward(self.grad, self)

                if (self.creation_op == "sub"):
                    self.creators[0].backward(Tensor(self.grad.data), self)
                    self.creators[1].backward(Tensor(self.grad.__neg__().data), self)

                if (self.creation_op == "mul"):
                    new = self.grad * self.creators[1]
                    self.creators[0].backward(new, self)
                    new = self.grad * self.creators[0]
                    self.creators[1].backward(new, self)

                if (self.creation_op == "mm"):
                    c0 = self.creators[0]
                    c1 = self.creators[1]
                    new = self.grad.mm(c1.transpose())
                    c0.backward(new)
                    new = self.grad.transpose().mm(c0).transpose()
                    c1.backward(new)

                if (self.creation_op == "transpose"):
                    self.creators[0].backward(self.grad.transpose())

                if ("sum" in self.creation_op):
                    dim = int(self.creation_op.split("_")[1])
                    self.creators[0].backward(self.grad.expand(dim,
                                                               self.creators[0].data.shape[dim]))

                if ("expand" in self.creation_op):
                    dim = int(self.creation_op.split("_")[1])
                    self.creators[0].backward(self.grad.sum(dim))

                if (self.creation_op == "neg"):
                    self.creators[0].backward(self.grad.__neg__())
    #加法
    def __add__(self, other):
        if (self.autograd and other.autograd):
            return Tensor(self.data + other.data,
                          autograd=True,
                          creators=[self, other],
                          creation_op="add")
        return Tensor(self.data + other.data)
    #取负
    def __neg__(self):
        if (self.autograd):
            return Tensor(self.data * -1,
                          autograd=True,
                          creators=[self],
                          creation_op="neg")
        return Tensor(self.data * -1)
    #减法
    def __sub__(self, other):
        if (self.autograd and other.autograd):
            return Tensor(self.data - other.data,
                          autograd=True,
                          creators=[self, other],
                          creation_op="sub")
        return Tensor(self.data - other.data)
    #乘法
    def __mul__(self, other):
        if (self.autograd and other.autograd):
            return Tensor(self.data * other.data,
                          autograd=True,
                          creators=[self, other],
                          creation_op="mul")
        return Tensor(self.data * other.data)
    #求和
    def sum(self, dim):
        if (self.autograd):
            return Tensor(self.data.sum(dim),  #即 Tensor 对象所存储的 numpy 数组数据)在指定维度 dim 上进行求和操作
                          autograd=True,
                          creators=[self],
                          creation_op="sum_" + str(dim))
        return Tensor(self.data.sum(dim))
    #扩展
    def expand(self, dim, copies):

        trans_cmd = list(range(0, len(self.data.shape)))
        trans_cmd.insert(dim, len(self.data.shape))
        new_data = self.data.repeat(copies).reshape(list(self.data.shape) + [copies]).transpose(trans_cmd)

        if (self.autograd):
            return Tensor(new_data,
                          autograd=True,
                          creators=[self],
                          creation_op="expand_" + str(dim))
        return Tensor(new_data)
    #转置
    def transpose(self):
        if (self.autograd):
            return Tensor(self.data.transpose(),
                          autograd=True,
                          creators=[self],
                          creation_op="transpose")

        return Tensor(self.data.transpose())
    #矩阵乘法
    def mm(self, x):
        if (self.autograd):
            return Tensor(self.data.dot(x.data),
                          autograd=True,
                          creators=[self, x],
                          creation_op="mm")
        return Tensor(self.data.dot(x.data))

    def __repr__(self):
        return str(self.data.__repr__())

    def __str__(self):
        return str(self.data.__str__())


class SGD(object):

    def __init__(self, parameters, alpha=0.1):
        self.parameters = parameters   #parameters: 这是一个包含模型参数的列表或迭代器。每个参数通常是一个包含 data 和 grad 属性的对象,data 是参数的当前值,grad 是参数的梯度。
        self.alpha = alpha             #alpha: 学习率(默认值为 0.1),控制每次参数更新的步长。
    '''
    作用:将模型参数的梯度 (grad.data) 设置为零。
    用途:在每次参数更新之前,通常需要将梯度清零,以避免梯度累积。
    如果不清零,梯度会不断累加,导致参数更新错误。
    '''
    def zero(self):
        for p in self.parameters:
            p.grad.data *= 0


    def step(self, zero=True):

        for p in self.parameters:

            p.data -= p.grad.data * self.alpha
            '''
            3. zero 方法:梯度清零
            功能:
            将每个参数的梯度 (grad.data) 置为零。
            用途:
            在每次参数更新之前,通常需要将梯度清零,以避免梯度累积。
            '''
            if (zero):
                p.grad.data *= 0
            '''
            1. 梯度清零的作用
            (1) 避免梯度累积
            在每次反向传播时,梯度是通过链式法则计算的,并存储在参数的 grad.data 中。            
            如果不清零,下一次反向传播时,新的梯度会累加到之前的梯度上,而不是覆盖之前的梯度。            
            这会导致梯度值越来越大,参数更新方向错误,训练过程不稳定。            
            (2) 确保梯度是当前批次的结果
            梯度清零的目的是确保每次参数更新时使用的梯度是当前批次的梯度。            
            如果不清零,梯度会包含之前批次的梯度信息,导致参数更新不准确。
            2. 不清零的后果
            假设我们有两个批次的数据,梯度分别为 g1 和 g2:            
            第一次反向传播:
            计算梯度 g1,并存储在 p.grad.data 中。            
            更新参数:p.data -= g1 * self.alpha。            
            第二次反向传播:            
            如果不清零,新的梯度 g2 会累加到 p.grad.data 中,即 p.grad.data = g1 + g2。            
            更新参数:p.data -= (g1 + g2) * self.alpha。            
            可以看到,第二次参数更新时使用的梯度是 g1 + g2,而不是 g2。这会导致以下问题:            
            梯度方向错误:参数更新方向偏离了当前批次的梯度方向。            
            梯度爆炸:梯度值会不断累加,导致参数更新步长过大,训练过程不稳定。            
            训练效果差:模型无法正确拟合数据,损失函数值可能不收敛。
            3. 为什么 p.grad.data 需要清零?
            在 PyTorch 或类似的深度学习框架中,梯度是通过反向传播累加到 p.grad.data 中的,而不是直接覆盖。这是因为:
            灵活性:累加梯度可以支持更复杂的优化策略,例如梯度累积(Gradient Accumulation)。            
            效率:累加梯度可以利用链式法则的自动微分机制,避免重复计算。            
            因此,每次参数更新后,需要手动将 p.grad.data 清零,以确保下一次反向传播时梯度是全新的。
            4. 梯度累积(Gradient Accumulation)
            虽然梯度清零是默认行为,但在某些场景下,我们可能希望累积梯度,而不是清零。例如:
            显存不足:当显存不足以支持较大的批量时,可以将数据分成多个小批次,累积梯度后再更新参数。            
            更稳定的更新:累积多个小批次的梯度可以减少梯度估计的方差,使参数更新更稳定。
            总结
            梯度清零是为了避免梯度累积,确保每次参数更新时使用的梯度是当前批次的结果。            
            如果不清零,梯度会不断累加,导致参数更新方向错误、梯度爆炸和训练效果差。            
            在某些场景下(如梯度累积),可以不清零梯度,但需要手动控制更新和清零的时机。
            '''

class Layer(object):

    def __init__(self):
        self.parameters = list()

    def get_parameters(self):
        return self.parameters

'''
功能:定义一个线性层,继承自 Layer 类。
作用:线性层是神经网络的核心组件之一,用于实现输入数据的线性变换。
'''
class Linear(Layer):

    def __init__(self, n_inputs, n_outputs):#初始化方法:__init__
        '''
        功能:调用父类 Layer 的初始化方法。
        作用:确保 Linear 类继承了 Layer 类的属性和方法。
        '''
        super().__init__()
        '''
        功能:初始化权重矩阵 W。
        参数:        
        n_inputs:输入特征的维度。        
        n_outputs:输出特征的维度。        
        细节:        
        使用 np.random.randn 生成一个形状为 (n_inputs, n_outputs) 的随机矩阵。        
        乘以 np.sqrt(2.0 / (n_inputs)) 是为了使用 He 初始化(He Initialization),
        这是一种常用的权重初始化方法,适用于 ReLU 激活函数。
        '''
        W = np.random.randn(n_inputs, n_outputs) * np.sqrt(2.0 / (n_inputs))
        self.weight = Tensor(W, autograd=True)
        '''
        功能:初始化偏置向量 bias。        
        细节:        
        偏置是一个形状为 (n_outputs,) 的零向量。        
        使用 Tensor 封装,并启用自动求导(autograd=True)
        '''
        self.bias = Tensor(np.zeros(n_outputs), autograd=True)
        '''
        功能:将权重和偏置添加到 self.parameters 列表中。
        作用:        
        self.parameters 是一个存储模型参数的列表。        
        在训练时,优化器(如 SGD)会通过 self.parameters 访问并更新这些参数。
        '''
        self.parameters.append(self.weight)
        self.parameters.append(self.bias)

    '''
    (1) 输入参数 input
    功能:接受输入数据。
    类型:input 是一个 Tensor 对象,表示输入数据。
    (2) 矩阵乘法 input.mm(self.weight)
    功能:计算输入数据与权重矩阵的矩阵乘法。    
    作用:    
    将输入数据从 n_inputs 维度映射到 n_outputs 维度。    
    这是线性变换的核心部分。
    (3) 扩展偏置 self.bias.expand(0, len(input.data))
    功能:将偏置向量扩展为与输入数据相同的形状。    
    作用:    
    偏置的原始形状是 (n_outputs,),而输入数据的形状是 (batch_size, n_inputs)。    
    通过扩展,偏置可以被加到每个样本的输出上。
    (4) 返回结果
    功能:返回线性变换的结果。
    作用:    
    输出是一个 Tensor 对象,形状为 (batch_size, n_outputs)。    
    这是线性层的最终输出。
    '''
    def forward(self, input):
        return input.mm(self.weight) + self.bias.expand(0, len(input.data))
    '''
    偏置(Bias)在神经网络中是一个非常重要的概念,它的存在与否对模型的表达能力有直接影响。
    下面我会详细解释为什么需要偏置,以及为什么通常将偏置初始化为零向量。
    1. 为什么需要偏置?
    (1) 增加模型的表达能力
    线性变换的定义:线性变换的形式是 y = Wx + b,其中:    
    W 是权重矩阵。    
    x 是输入数据。    
    b 是偏置向量。    
    偏置的作用:    
    偏置 b 允许模型在输入为 0 时输出非零值。    
    如果没有偏置,模型的输出只能是 y = Wx,这意味着当 x = 0 时,y 也必须为 0。这会限制模型的表达能力。    
    偏置的存在使得模型可以学习到更复杂的映射关系。
    (2) 举例说明
    假设我们有一个简单的线性模型:
    输入 x 是一个标量。
    输出 y 也是一个标量。
    模型的形式是 y = wx + b。
    情况 1:没有偏置(b = 0)
    模型的形式是 y = wx。
    无论 w 如何调整,模型都必须通过原点 (0, 0)。
    如果真实数据的分布不经过原点,模型将无法很好地拟合数据。
    情况 2:有偏置(b ≠ 0)
    模型的形式是 y = wx + b。    
    通过调整 w 和 b,模型可以拟合任意斜率和截距的直线。    
    模型的表达能力更强,可以更好地拟合数据。    
    (3) 非线性激活函数
    在神经网络中,线性层通常后面会接一个非线性激活函数(如 ReLU)。    
    偏置的存在可以帮助激活函数更好地发挥作用。    
    例如,ReLU 的形式是 max(0, x),偏置可以调整输入的分布,使得激活函数更容易激活。
    2. 为什么将偏置初始化为零向量?
    (1) 初始化为零的常见做法
    在深度学习中,偏置通常初始化为零向量。
    这是因为:
    偏置的初始值对模型的初始状态影响较小。
    初始化为零可以避免在训练初期引入不必要的偏差。
    (2) 权重和偏置的初始化区别
    权重初始化:    
    权重通常使用随机初始化(如 He 初始化或 Xavier 初始化)。    
    这是因为权重的初始值对模型的初始状态影响较大,随机初始化可以帮助打破对称性,加速训练。    
    偏置初始化:    
    偏置通常初始化为零。    
    这是因为偏置的作用是调整输出的偏移量,初始化为零可以让模型从“中性”状态开始学习。
    (3) 偏置初始化为零的实际效果
    初始化为零的偏置不会对模型的初始输出产生影响。
    在训练过程中,偏置会通过梯度下降逐步调整,以学习数据中的偏移量。
    3. 如果偏置设置为 0,是否可以不加偏置?
    (1) 偏置设置为 0 的效果
    如果偏置设置为 0,那么模型的形式就是 y = Wx。    
    这意味着模型必须通过原点 (0, 0),这会限制模型的表达能力。
    (2) 是否可以不加偏置?
    在某些特殊情况下,可以不加偏置:    
    数据经过标准化:如果输入数据已经经过标准化(均值为 0),且输出数据也经过标准化,那么偏置的作用可能会减弱。    
    特定任务:在某些任务中(如某些类型的嵌入层),偏置的作用可能不明显。    
    但在大多数情况下,偏置是必要的,因为它可以增加模型的表达能力。
    (3) 实际应用
    在深度学习中,偏置通常是默认添加的。    
    即使在某些情况下偏置的作用不明显,添加偏置也不会对模型产生负面影响。    
    如果偏置确实不必要,可以通过训练过程自动学习到接近 0 的值。
    4. 代码示例
    以下是一个简单的示例,展示了偏置的作用:
    # 定义一个线性层,带偏置
    class LinearWithBias:
        def __init__(self, n_inputs, n_outputs):
            self.weight = np.random.randn(n_inputs, n_outputs)
            self.bias = np.zeros(n_outputs)
    
        def forward(self, x):
            return np.dot(x, self.weight) + self.bias

    # 定义一个线性层,不带偏置
    class LinearWithoutBias:
        def __init__(self, n_inputs, n_outputs):
            self.weight = np.random.randn(n_inputs, n_outputs)
    
        def forward(self, x):
            return np.dot(x, self.weight)

    # 测试
    x = np.array([[1.0, 2.0]])  # 输入数据
    
    # 带偏置的线性层
    linear_with_bias = LinearWithBias(2, 1)
    y_with_bias = linear_with_bias.forward(x)
    print("With bias:", y_with_bias)
    
    # 不带偏置的线性层
    linear_without_bias = LinearWithoutBias(2, 1)
    y_without_bias = linear_without_bias.forward(x)
    print("Without bias:", y_without_bias)
    
    5. 总结
    偏置的作用:增加模型的表达能力,允许模型在输入为 0 时输出非零值。    
    偏置初始化为零:这是常见的做法,可以让模型从“中性”状态开始学习。    
    是否可以不加偏置:在某些特殊情况下可以不加偏置,但在大多数情况下,偏置是必要的。
    '''

#1. 类的定义:Sequential
'''
功能:定义一个顺序模型,继承自 Layer 类。
作用:顺序模型是神经网络的核心组件之一,用于将多个层按顺序连接起来。
'''
class Sequential(Layer):

    def __init__(self, layers=list()):#2. 初始化方法:__init__
        '''
        (1) super().__init__()
        功能:调用父类 Layer 的初始化方法。
        作用:确保 Sequential 类继承了 Layer 类的属性和方法。
        '''
        super().__init__()

        '''
        (2) self.layers = layers
        功能:初始化层的列表。        
        作用:存储顺序模型中的所有层。
        '''
        self.layers = layers
    '''
    功能:向顺序模型中添加一个层。
    参数:    
    layer:要添加的层(例如 Linear 层)。    
    作用:将层添加到 self.layers 列表中。
    '''
    def add(self, layer): #3. 添加层的方法:add
        self.layers.append(layer)

    '''
    4. 前向传播方法:forward
    (1) 输入参数 input
    功能:接受输入数据。    
    类型:input 是一个 Tensor 对象,表示输入数据。
    
    '''
    def forward(self, input):
        '''
        功能:按顺序调用每一层的前向传播方法。
        作用:
        将输入数据依次传递给每一层。
        每一层的输出作为下一层的输入。
        '''
        for layer in self.layers:
            input = layer.forward(input)
        return input
    '''
    5. 获取参数的方法:get_parameters
    '''
    def get_parameters(self):
        params = list() #功能:初始化一个空列表,用于存储所有参数。
        '''
        遍历所有层
        功能:调用每一层的 get_parameters 方法,获取该层的参数。
        作用:        
        将每一层的参数添加到 params 列表中。 
        '''
        for l in self.layers:
            params += l.get_parameters()
        return params #功能:返回顺序模型的所有参数。

np.random.seed(1)

data = Tensor(np.array([[0, 0], [0, 1], [1, 0], [1, 1]]), autograd=True)
target = Tensor(np.array([[0], [1], [0], [1]]), autograd=True)

w = list()
'''
w.append(Tensor(np.random.rand(2, 3), autograd=True)) 这行代码的主要功能是创建一个形状为 (2, 3) 的随机张量(Tensor),
并将其添加到列表 w 中。这个随机张量的数据是从均匀分布 [0, 1) 中随机采样得到的,同时开启了自动求导(autograd=True)功能,
意味着后续可以对该张量进行梯度计算,常用于深度学习模型的参数初始化。
'''
weights_0_1 = np.array([[0.1, 0.2, 0.3],
                        [0.2, 0.3, 0.4]])

weights_1_2 = np.array([[0.1], [0.2], [0.3]])
w.append(Tensor(weights_0_1, autograd=True))
w.append(Tensor(weights_1_2, autograd=True))
#模型和优化器的初始化
'''
创建顺序模型
Sequential([Linear(2, 3), Linear(3, 1)]):
定义了一个包含两层的顺序模型。
第一层:Linear(2, 3),输入维度为 2,输出维度为 3。
第二层:Linear(3, 1),输入维度为 3,输出维度为 1。
'''
model = Sequential([Linear(2, 3), Linear(3, 1)])
'''
(2) 创建优化器
SGD(parameters=model.get_parameters(), alpha=0.05):
使用随机梯度下降(SGD)优化器。
parameters=model.get_parameters():获取模型的所有参数(权重和偏置)。
alpha=0.05:学习率为 0.05。
'''
optim = SGD(parameters=model.get_parameters(), alpha=0.05)


#训练循环
for i in range(10):
    # Predict
    '''
    (1) 预测(前向传播)
    功能:通过模型的前向传播计算预测值。
    输入:data 是一个形状为 (4, 2) 的张量,表示 4 个样本,每个样本有 2 个特征。    
    输出:pred 是一个形状为 (4, 1) 的张量,表示模型对 4 个样本的预测值。
    '''
    pred = model.forward(data)
    '''
    (2) 计算损失
    功能:计算损失函数。
    损失函数:这里使用的是均方误差(MSE)损失函数。    
    公式:loss = sum((pred - target)^2)    
    输入:    
    pred:模型的预测值,形状为 (4, 1)。    
    target:目标值,形状为 (4, 1)。    
    输出:loss 是一个标量(单个数值),表示所有样本的总损失。
    '''
    # Compare
    loss = ((pred - target) * (pred - target)).sum(0)

    '''
    (3) 反向传播
    功能:通过反向传播计算梯度。
    输入:    
    Tensor(np.ones_like(loss.data)):这是一个形状与 loss 相同的张量,所有元素为 1,表示损失对自身的梯度为 1。
    输出:计算并存储每个参数的梯度(grad)。
    '''
    # Learn
    loss.backward(Tensor(np.ones_like(loss.data)))
    '''
    (4) 更新参数
    功能:更新模型参数。
    更新规则:p.data -= p.grad.data * self.alpha    
    p.data:参数的当前值。    
    p.grad.data:参数的梯度。    
    self.alpha:学习率。
    '''
    optim.step()
    '''
    (5) 打印损失
    功能:打印当前批次的损失值。
    作用:观察训练过程中损失值的变化。
    '''
    print(loss)

'''
[4.33222765]
[0.06584977]
[0.01869537]
[0.01068846]
[0.00609207]
[0.00360451]
[0.00210719]
[0.00126275]
[0.00075884]
[0.00046488]
'''
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐