首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >深层神经网络在Python3中的实现

深层神经网络在Python3中的实现
EN

Code Review用户
提问于 2019-05-02 12:44:54
回答 1查看 241关注 0票数 8

我用简单的反向传播训练实现了任意前馈神经网络的第一个实现。

代码语言:javascript
复制
class NeuralNet(object):
    learning_rate = .1

    # Error function
    @staticmethod
    def J(guess, target):
        return 0.5 * np.linalg.norm(guess - target, 2) ** 2

    def __init__(self, structure=tuple(), activation_functions=tuple(), activation_derivatives=tuple()):
        self.w = []
        self.b = []
        self.f = list(activation_functions)
        self.df = list(activation_derivatives)

        li = len(structure) - 1
        while li > 0:
            self.w.insert(0, np.random.uniform(low=-1.0, high=1.0, size=(structure[li - 1], structure[li])))
            self.b.insert(0, np.random.uniform(low=-1.0, high=1.0, size=structure[li]))

            li -= 1

    def forward(self, x):
        z = [0] * len(self.w)
        a = [x]

        for i, (wi, bi, fi) in enumerate(zip(self.w, self.b, self.f)):
            z[i] = a[i] @ wi + bi
            a.append(fi(z[i]))

        return a[-1]

    def train(self, training_data, labels):
        # Get a permutation vector for shuffling the inputs and labels in each epoch:
        permutation = np.random.permutation(len(inputs))

        # Keeping track of all MSE values:
        errors = []

        # Training loop:
        for epoch in range(10000):

            # Shuffling the inputs and labels for each epoch:
            X = training_data[permutation]
            Y = labels[permutation]

            #                                          n
            # Keeping track of the error: MSE = 1/n * SUM ||Activation - Target|| ** 2
            #                                         i=1
            error = 0.0
            for xi, yi in zip(X, Y):
                # Forward pass:
                z = [0] * len(self.w)
                a = [xi]

                for i, (wi, bi, fi) in enumerate(zip(self.w, self.b, self.f)):
                    z[i] = a[i] @ wi + bi
                    a.append(fi(z[i]))

                # Calculate error for (xi, yi) according to 0.5 * ||xi - yi|| ** 2
                error += self.J(a[-1], yi)

                # Backwards pass:
                #  - Calculate deltas:
                layer_delta = []
                iter_zi = iter(zip(reversed(range(len(z))), reversed(z)))
                layer_delta.insert(0, (a[-1] - yi) * self.df[1](next(iter_zi)[1]))
                for i, zi in iter_zi:
                    delta_i = np.multiply(layer_delta[0] @ self.w[i+1].T, self.df[i](z[i]))
                    layer_delta.insert(0, delta_i)

                #  - Calculate weight deltas:
                weight_delta = []
                for i, di in zip(reversed(range(len(layer_delta))), reversed(layer_delta)):
                    delta_wi = a[i].reshape(-1, 1) * layer_delta[i]
                    weight_delta.insert(0, delta_wi)

                # w[i](new) := w[i](old) - LR * dJ/dw[i]
                # b[i](new) := b[i](old) - LR * dJ/db[i]
                for i in range(len(self.w)):
                    self.w[i] = self.w[i] - self.learning_rate * weight_delta[i]
                    self.b[i] = self.b[i] - self.learning_rate * layer_delta[i]

            errors.append(error / len(X))

            # Convergence testing, according to the last N errors:
            error_delta = sum(reversed(errors[-5:]))
            if error_delta < 1.0e-6:
                print("Error delta reached, ", epoch, " exiting.")
                break

        return errors

这似乎工作得很好,至少它成功地学习了XOR问题:

代码语言:javascript
复制
# Inputs and their respective labels:
inputs = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
labels = np.array([[0], [1], [1], [0]])

nn = NeuralNet(structure=(2, 2, 1),
               activation_functions=(lambda x: np.tanh(x),
                                     lambda x: .25 * x if x < 0 else x),
               activation_derivatives=(lambda x: 1.0 - np.tanh(x) ** 2,
                                       lambda x: .25 if x < 0 else 1))
nn.learning_rate = 0.1
print("Before training:")
print(nn.forward(np.array([0, 0])))
print(nn.forward(np.array([0, 1])))
print(nn.forward(np.array([1, 0])))
print(nn.forward(np.array([1, 1])))

errors = nn.train(inputs, labels)

print("After training:")
print(nn.forward(np.array([0, 0])))
print(nn.forward(np.array([0, 1])))
print(nn.forward(np.array([1, 0])))
print(nn.forward(np.array([1, 1])))

plt.plot(errors)
plt.show()

layer_deltas的计算似乎特别复杂,但每当我试图简化它时,我就会破坏一些东西。任何我可以简化的提示,还是上面代码的任何提示?

EN

回答 1

Code Review用户

回答已采纳

发布于 2019-05-02 16:18:05

不那么任意的网络

你最初的说法是你的网络是“武断的”。据我所见,我倾向于说,这并不像人们想象的那样武断。

任意:

  • 每层神经元数量(你称之为“结构”)
  • 激活函数
  • 它们的衍生物(这意味着它们不是完全武断的)

不是任意的:

  • 学习速率(0.1)
  • 网络权值初始化
  • 误差函数(以某种方式设置为平方欧几里德距离的一半?)也许你想要均方误差在这里?)
  • 训练中的划时代数(10000)
  • 训练模式(样本与批次学习)
  • 培训终止条件(error_delta < 1.0e-6)
  • 优化器
  • ..。

这些都是(或多或少)重要的方面,不需要"猴补“就可以调到原始类中(如果可能的话)。这些参数中的一些参数可以很容易地在构造函数中设置,或者在调用train时(针对划期数等)设置。错误函数也可以很容易地在初始化时设置,特别是因为它已经是一个不依赖于类内部的@staticmethod

我假设一个灵活的优化器将超出您想要做的范围(并且有一个丰满 框架可以这样做)。

代码本身

网络权重和偏差的初始化使用while循环,这是不必要的复杂。这里不需要while循环,因为您知道需要执行多少次迭代。因此,使用for循环代替,这将导致您到:

代码语言:javascript
复制
for i in range(len(structure)-1):
    self.w.append(np.random.uniform(low=-1.0, high=1.0, size=(structure[i], structure[i+1])))
    self.b.append(np.random.uniform(low=-1.0, high=1.0, size=(structure[i+1])))

除了不需要继续跟踪索引变量之外,您还不需要使用带有负数索引的索引技巧,而且您可以使用.append(...)而不是插入前面。

哦,当我们处于初始化阶段时:没有必要将函数/派生元组转换为列表。您可以使用与over相同的方式来遍历/索引元组。

说到迭代,这个

代码语言:javascript
复制
layer_delta = []
iter_zi = iter(zip(reversed(range(len(z))), reversed(z)))
layer_delta.insert(0, (a[-1] - yi) * self.df[1](next(iter_zi)[1]))
for i, zi in iter_zi:
    delta_i = np.multiply(layer_delta[0] @ self.w[i+1].T, self.df[i](z[i]))
    layer_delta.insert(0, delta_i)

是疯狂的™IMHO (而且也可能有™的错误)。如果您真的认为需要,请参阅这是如此的帖子关于如何反向枚举Python列表的内容。我不认为您需要这样做,因为您只使用了实际的list值一次,在所有其他情况下都使用了索引。会把那个怪物弄成这样

代码语言:javascript
复制
layer_delta = [(a[-1] - yi) * self.df[-1](z[-1])]
for i in reversed(range(len(z)-1)):
    delta_i = np.multiply(layer_delta[0] @ self.w[i+1].T, self.df[i](z[i]))
    layer_delta.insert(0, delta_i)

你可以,也应该调整你的其他形式的怪物,以及上述。在对代码的这些部分进行重构时,我不能保证在解码代码时没有犯错误。

前传的实现也有点复杂的IMHO。因为您只通过网络一次,所以不需要存储所有层的激活和输出。你只需要最后一个。

您还在它自己的forward函数和train中实现了两次前传。如果您坚持原来的实现,请考虑是否还想返回其他值。这将只剩下一段代码可以修复,如果有问题。如果您担心可用性,因为应用程序阶段现在看到了所有的内部值,那么您可以实现一个内部函数,例如_forward,它做繁重的工作,让forward只返回最终的输出。

的注释和变量名

代码语言:javascript
复制
# Convergence testing, according to the last N errors:
error_delta = sum(reversed(errors[-5:]))
if error_delta < 1.0e-6:
    print("Error delta reached, ", epoch, " exiting.")
    break

也有点离谱。终止准则似乎确实检查最后一个N(其中N硬编码为5)的和是否低于您(任意选择的)阈值。此外,自和不关心订单以来,没有必要在这里反转列表。

由于您使用的是Python 3,所以还可以使用新的f-string语法进行输出格式化:

代码语言:javascript
复制
print(f"Error delta reached in {epoch} exiting.")

说到注释,您的类的方法都缺乏用户可见的文档。为此,Python程序员通常在他们的方法/类/函数上使用所谓的"""doc strings"""。例如:

代码语言:javascript
复制
def train(self, training_data, labels):
    """Train the neural network with all the available data

    The training continues until the maximum number of epochs is reached or
    the termination criterion is hit.
    """
    # your code here ...

用这种格式编写的文档将由所有主要的Python以及Python的内置help(...)函数来获取。

我知道在神经网络社区中有一些常见的命名约定,当你实现它的时候,你应该尽可能严格地遵守它们,这是你通常所做的。<#>但请记住,不要牺牲代码的清晰度和可读性。例如,z可以变成activationa也可以被命名为layer_output (复数可能适用于用作列表的地方)。

票数 9
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/219583

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档