原文地址为什么使用RNN？递归神经网络（Recurrent Neural Networks，RNN）是一种专门处理序列的神经网络。由于它们在处理文本方面的有效性，因此经常用于自然语言处理（NLP）任务。在本文中，我们将探讨RNN是什么，了解它们的工作原理，并在Python中从头开始构建一个真正的RNN。（仅使用numpy）。这篇文章假定您具有神经网络的基础知识。建议先阅读神经网络基础最原始（vanilla，原意为香草，在机器学习中等同于raw）的神经网络和CNNs存在一个问题：只能接受固定的输入并产生固定的输出。RNN则不同，它可以接受并输出可变长度序列，以下是RNN的一些例子： 输入为红色，RNN本身为绿色，输出为蓝色。资料来源：Andrej Karpathy这种处理序列的能力使RNN非常有用。例如：机器翻译（例如Google Translate）使用“多对多” 型RNN进行。原始文本序列输送到RNN，然后RNN生成翻译后的文本并输出。情绪分析（例如，这是正面还是负面的评价？）通常使用“多对一”的RNN进行。将要分析的文本输入到RNN中，然后生成一个单一的输出分类（例如，这是正面的评价）。在本文的后半部分，我们将构建一个“多对一”式的RNN，实现一个最简单的情感分析功能如何做？让我们考虑一个“多对多”式的RNN，它接受$x _ { 0 } , x _ { 1 } , \dots x _ { n }$的输入，并产生$y _ { 0 } , y _ { 1 } , \dots y _ { n }$的输出，xi和yi可以是任意维度的向量。RNN借助一个迭代更新的 隐状态（hidden state）h来工作，h也是一个任意维度的向量。在任意给定的步长t上，有：下一个 隐状态$h_t$通过先前的 隐状态$h_{t-1}$和下一个输入 $x_t$计算而来。下一个输出$y_t$由$h_t$计算而来。 一个多对多RNN这就是使RNN 反复出现的原因：它对每个步骤使用相同的权重。更具体地说，典型的原始RNN仅使用3组权重来执行其计算：$W_{xh}$用于所有的$x_t\to h_t$链接$W_{hh}$用于所有的$h_{t-1}\to h_t$链接$W_{hy}$用于所有的$h_t\to y_t$链接同时我们为RNN应用两个偏置（biases）：$b_h$，计算$h_t$的时候需要加上它。$b_y$，计算$y_t$的时候需要加上它。我们用矩阵表示权重，用向量表示偏置。这3个权重和2个偏置构成了整个RNN！以下是将所有内容组合在一起的方程式：$$\begin{array} { c } { h _ { t } = \tanh \left( W _ { x h } x _ { t } + W _ { h h } h _ { t - 1 } + b _ { h } \right) } \\ { y _ { t } = W _ { h y } h _ { t } + b _ { y } } \end{array}$$不要跳过这些方程式。停下来思考一下。另外，请记住，权重是矩阵，其他变量是向量。所有的权重都使用矩阵乘法，并且将偏置加到乘积结果中。然后，我们将tanh用作第一个方程式的激活函数（但是也可以使用S型等其他激活函数）。问题我们将从头开始实现RNN，以执行简单的情感分析任务：确定给定的文本字符串是正面的还是负面的。这是我为本文汇总的小型数据集的一部分：TextPositive?i am good✓i am bad❌this is very good✓this is not bad✓i am bad not good❌i am not at all happy❌this was good earlier✓i am not at all bad or sad right now✓计划由于这是一个分类问题，因此我们将使用“多对一” RNN。这类似于我们之前讨论的“多对多” RNN，但是它仅使用最终的 隐状态来输出y 多对一RNN每 个$x_i$将是一个向量，代表文本中的一个单词。输出$y$是一个包含两个数字的向量，一个代表正面，另一个代表负面。我们将用Softmax将这些值转换为概率，并最终确定正负。让我们开始构建我们的RNN！预处理我前面提到的数据集由两个Python字典组成：train_data = { 'good': True, 'bad': False, # ... more data } test_data = { 'this is happy': True, 'i am good': True, # ... more data }True = Positive, False = Negative我们必须进行一些预处理才能将数据转换为可用格式。首先，我们将构建一个包含了数据中所有单词的词汇表：from data import train_data, test_data # Create the vocabulary. vocab = list(set([w for text in train_data.keys() for w in text.split(' ')])) vocab_size = len(vocab) print('%d unique words found' % vocab_size) # 18 unique words foundvocab现在包含训练文本中的所有单词的列表。接下来，我们将分配一个整数索引来表示vocab中的每个单词。# Assign indices to each word. word_to_idx = { w: i for i, w in enumerate(vocab) } idx_to_word = { i: w for i, w in enumerate(vocab) } print(word_to_idx['good']) # 16 (this may change) print(idx_to_word[0]) # sad (this may change)译者注：出现Reloaded modules: data错误是正常的，不影响使用，实在不想看到红字，可以注释掉from data这一句现在，我们可以用其相应的整数索引表示任何给定的单词！这是必要的，因为RNN无法理解单词-我们必须给它们编号。最后，回想一下，每个输入RNN的$x_i$都是向量。我们将使用one-hot向量（即独热编码），每个向量中只含有一个1，其他的全是0。向量中的1位于单词对应的整数索引处。对独热编码不了解的同学，可以参考这篇文章，讲得挺清楚的。由于我们的词汇表中有18个独特的单词，因此每个单词$x_i$将是一个18维的独热矢量。import numpy as np def createInputs(text): ''' Returns an array of one-hot vectors representing the words in the input text string. - text is a string - Each one-hot vector has shape (vocab_size, 1) ''' inputs = [] for w in text.split(' '): v = np.zeros((vocab_size, 1)) v[word_to_idx[w]] = 1 inputs.append(v) return inputs前向阶段是时候应用我们的RNN了！我们将从初始化RNN所需的3个权重和2个偏置开始：import numpy as np from numpy.random import randn class RNN: # A Vanilla Recurrent Neural Network. def __init__(self, input_size, output_size, hidden_size=64): # Weights self.Whh = randn(hidden_size, hidden_size) / 1000 self.Wxh = randn(hidden_size, input_size) / 1000 self.Why = randn(output_size, hidden_size) / 1000 # Biases self.bh = np.zeros((hidden_size, 1)) self.by = np.zeros((output_size, 1))注意：我们将除以1000以减小权重的初始方差。这不是初始化权重的最佳方法，但它很简单并且适用于本文。我们使用np.random.randn()从标准正态分布中初始化权重。接下来，让我们实现RNN的前向传递。还记得我们前面看到的这两个方程吗？$$\begin{array} { c } { h _ { t } = \tanh \left( W _ { x h } x _ { t } + W _ { h h } h _ { t - 1 } + b _ { h } \right) } \\ { y _ { t } = W _ { h y } h _ { t } + b _ { y } } \end{array}$$下面是代码实现：class RNN: # ... def forward(self, inputs): ''' Perform a forward pass of the RNN using the given inputs. Returns the final output and hidden state. - inputs is an array of one hot vectors with shape (input_size, 1). ''' h = np.zeros((self.Whh.shape[0], 1)) # Perform each step of the RNN for i, x in enumerate(inputs): h = np.tanh(self.Wxh @ x + self.Whh @ h + self.bh) # Compute the output y = self.Why @ h + self.by return y, h很简单，对吧？注意我们第一步将h初始化为零向量，因为之前没有h可供我们使用。让我们尝试一下：def softmax(xs): # Applies the Softmax Function to the input array. return np.exp(xs) / sum(np.exp(xs)) # Initialize our RNN! rnn = RNN(vocab_size, 2) inputs = createInputs('i am very good') out, h = rnn.forward(inputs) probs = softmax(out) print(probs) # [[0.50000095], [0.49999905]]如果您需要有关Softmax的复习，请阅读我对Softmax的简单解释我们的RNN可以用，但还不是很有用。让我们改变一下……后向阶段为了训练RNN，我们需要一个损失函数。我们使用交叉熵损失函数（cross-entropy loss），它通常与Softmax相结合。下面是计算它的公式：$$L = - \ln \left( p _ { c } \right)$$其中$p_c$是我们的RNN对正确类别（正面或负面）的预测概率。例如，如果我们的RNN预测正面文本有90％的可能为正面，则损失为：$$L = - \ln \left(0.9 \right)=0.105$$想要更多解释？参考 CNNs中关于交叉熵损失 部分的介绍。现在我们的目标就是使用梯度下降法（gradient descent）训练CNN，以最小化损失。这意味着是时候对梯度求导了！以下部分假定了您有多元微积分的基本知识。您可以根据需要跳过它，但是即使您不太了解，我还是建议您略读一下。Ready? Here we go.定义首先，做一些定义：令y代表我们RNN的原始输出。令p为最终概率：$p = \text{softmax}(y)$。令c为某个文本集的真实标签（也称为“正确”类）。令L为CNN的交叉熵损失：$L = -\ln(p_c)$令$W_{xh}$, $W_{hh}$, 和 $W_{hy}$为RNN中的3个权重矩阵。令$b_h$ 和 $b_y$为RNN的2个偏置向量。设定接下来，我们需要编辑前向阶段以缓存一些数据，供后向阶段使用。在此过程中，我们还将为我们的后向阶段设置框架（skeleton）。代码如下：# Header: rnn.py class RNN: # ... def forward(self, inputs): ''' Perform a forward pass of the RNN using the given inputs. Returns the final output and hidden state. - inputs is an array of one hot vectors with shape (input_size, 1). ''' h = np.zeros((self.Whh.shape[0], 1)) # highlight-start self.last_inputs = inputs self.last_hs = { 0: h } # highlight-end # Perform each step of the RNN for i, x in enumerate(inputs): h = np.tanh(self.Wxh @ x + self.Whh @ h + self.bh) self.last_hs[i + 1] = h # highlight-line # Compute the output y = self.Why @ h + self.by return y, h # highlight-start def backprop(self, d_y, learn_rate=2e-2): ''' Perform a backward pass of the RNN. - d_y (dL/dy) has shape (output_size, 1). - learn_rate is a float. ''' pass # highlight-end对为什么要缓存感到疑惑？我们在CNN的训练概述 中介绍过，这里的原因类似。梯度数学时间到！我们从计算 $\frac{\partial L}{\partial y}$开始，我们知道：$$ L = -\ln(p_c) = -\ln(\text{softmax}(y_c)) $$我将保留实际的使用链式法则的求导 $\frac{\partial L}{\partial y}$作为你的练习，但结果非常好：$$ \frac{\partial L}{\partial y_i} = \begin{cases} p_i & \text{if $i \neq c$} \\ p_i - 1 & \text{if $i = c$} \\ \end{cases} $$例如，如果我们有 $p = [0.2, 0.2, 0.6]$ ，正确的组是 $c = 0$, 那么有 $\frac{\partial L}{\partial y} = [-0.8, 0.2, 0.6]$. 代码实现也非常简单：# Header: main.py # Loop over each training example for x, y in train_data.items(): inputs = createInputs(x) target = int(y) # Forward out, _ = rnn.forward(inputs) probs = softmax(out) # Build dL/dy # highlight-start d_L_d_y = probs d_L_d_y[target] -= 1 # highlight-end # Backward rnn.backprop(d_L_d_y) # highlight-line非常好，接下来我们探讨$W_{hy}$和 $b_y$的梯度，它们仅用于将最终的隐状态（hidden state）转换为RNN的输出。我们有：$$ \frac{\partial L}{\partial W_{hy}} = \frac{\partial L}{\partial y} * \frac{\partial y}{\partial W_{hy}} $$$$ y = W_{hy} h_n + b_y $$其中 $h_n$ 是最终的隐状态。$$ \frac{\partial y}{\partial W_{hy}} = h_n $$$$ \frac{\partial L}{\partial W_{hy}} = \boxed{\frac{\partial L}{\partial y} h_n} $$类似地，$$ \frac{\partial y}{\partial b_y} = 1 $$$$ \frac{\partial L}{\partial b_y} = \boxed{\frac{\partial L}{\partial y}} $$我们现在可以实现backprop()了!# Header: rnn.py class RNN: # ... def backprop(self, d_y, learn_rate=2e-2): ''' Perform a backward pass of the RNN. - d_y (dL/dy) has shape (output_size, 1). - learn_rate is a float. ''' n = len(self.last_inputs) # Calculate dL/dWhy and dL/dby. # highlight-start d_Why = d_y @ self.last_hs[n].T d_by = d_y # highlight-end提醒:在之前的 forward() 我们已经创建了self.last_hs .最后，我们需要 $W_{hh}$, $W_{xh}$,和 $b_h$的梯度, 它们被用于RNN的每一步：$$ \frac{\partial L}{\partial W_{xh}} = \frac{\partial L}{\partial y} \sum_t \frac{\partial y}{\partial h_t} * \frac{\partial h_t}{\partial W_{xh}} $$因为改变$W_{xh}$ 会影响到每一个 $h_t$, 它们都会影响y和最终的L。为了完全计算$W_{xh}$的梯度, 我们需要通过所有的时间步（timesteps）进行反向传播, 这种方法被称为”基于时间的反向传播算法“（Backpropagation Through Time，BPTT）: 基于时间的反向传播算法$W_{xh}$用于所有 $x_t→h_t$的前向链接，因此我们必须反向传播回每个链接。一旦我们到达给定的步骤t，我们需要计算$\frac{\partial h_t}{\partial W_{xh}}$：$$ h_t = \tanh (W_{xh} x_t + W_{hh} h_{t-1} + b_h) $$$\tanh$ 的导数众所周知:$$ \frac{d \tanh(x)}{dx} = 1 - \tanh^2(x) $$同样进行链式求导：$$ \frac{\partial h_t}{\partial W_{xh}} = \boxed{(1 - h_t^2) x_t} $$类似地，$$ \frac{\partial h_t}{\partial W_{hh}} = \boxed{(1 - h_t^2) h_{t-1}} $$$$ \frac{\partial h_t}{\partial b_h} = \boxed{(1 - h_t^2)} $$我们需要的最后一个导数是 $\frac{\partial y}{\partial h_t}$. 我们可以递归地计算它:$$ \begin{aligned} \frac{\partial y}{\partial h_t} &= \frac{\partial y}{\partial h_{t+1}} * \frac{\partial h_{t+1}}{\partial h_t} \\ &= \frac{\partial y}{\partial h_{t+1}} (1 - h_t^2) W_{hh} \\ \end{aligned} $$我们从最后一个隐状态开始实施BPTT并后向工作, 所以在我们想计算$\frac{\partial y}{\partial h_t}$时我们已经有了 $\frac{\partial y}{\partial h_{t+1}}$ ! 最后一个隐状态, $h_n$是例外:$$ \frac{\partial y}{\partial h_n} = W_{hy} $$我们现在有实现BTPP的一切条件，代码如下：# Header: rnn.py class RNN: # ... def backprop(self, d_y, learn_rate=2e-2): ''' Perform a backward pass of the RNN. - d_y (dL/dy) has shape (output_size, 1). - learn_rate is a float. ''' n = len(self.last_inputs) # Calculate dL/dWhy and dL/dby. d_Why = d_y @ self.last_hs[n].T d_by = d_y # Initialize dL/dWhh, dL/dWxh, and dL/dbh to zero. d_Whh = np.zeros(self.Whh.shape) d_Wxh = np.zeros(self.Wxh.shape) d_bh = np.zeros(self.bh.shape) # Calculate dL/dh for the last h. d_h = self.Why.T @ d_y # Backpropagate through time. for t in reversed(range(n)): # An intermediate value: dL/dh * (1 - h^2) temp = ((1 - self.last_hs[t + 1] ** 2) * d_h) # dL/db = dL/dh * (1 - h^2) d_bh += temp # highlight-line # dL/dWhh = dL/dh * (1 - h^2) * h_{t-1} d_Whh += temp @ self.last_hs[t].T # highlight-line # dL/dWxh = dL/dh * (1 - h^2) * x d_Wxh += temp @ self.last_inputs[t].T # highlight-line # Next dL/dh = dL/dh * (1 - h^2) * Whh d_h = self.Whh @ temp # Clip to prevent exploding gradients. for d in [d_Wxh, d_Whh, d_Why, d_bh, d_by]: np.clip(d, -1, 1, out=d) # Update weights and biases using gradient descent. self.Whh -= learn_rate * d_Whh self.Wxh -= learn_rate * d_Wxh self.Why -= learn_rate * d_Why self.bh -= learn_rate * d_bh self.by -= learn_rate * d_by注意事项：为了方便，我们合并$\frac{\partial L}{\partial y} * \frac{\partial y}{\partial h}$ 为 $\frac{\partial L}{\partial h}$ .我们不断更新变量 d_h ，它包含最新的 $\frac{\partial L}{\partial h_{t+1}}$, 我们需要计算 $\frac{\partial L}{\partial h_t}$.完成BPTT后, 我们使用np.clip() 方法对小于-1或大于1的梯度值进行裁剪，这有助于缓解爆炸性梯度（exploding gradient）问题，即包含大量相乘项时梯度会变得非常大。梯度消失问题对于原始RNN来说是一个很大的问题-像LSTM这样的更复杂的RNN通常能更容易地解决这些问题。计算完所有梯度后，我们将使用梯度下降更新权重和偏差。我们的RNN完成了！测试首先，我们将编写一个辅助函数来使用RNN处理数据：# Header: main.py import random def processData(data, backprop=True): ''' Returns the RNN's loss and accuracy for the given data. - data is a dictionary mapping text to True or False. - backprop determines if the backward phase should be run. ''' items = list(data.items()) random.shuffle(items) loss = 0 num_correct = 0 for x, y in items: inputs = createInputs(x) target = int(y) # Forward out, _ = rnn.forward(inputs) probs = softmax(out) # Calculate loss / accuracy loss -= np.log(probs[target]) num_correct += int(np.argmax(probs) == target) if backprop: # Build dL/dy d_L_d_y = probs d_L_d_y[target] -= 1 # Backward rnn.backprop(d_L_d_y) return loss / len(data), num_correct / len(data)现在，我们可以编写训练循环：# Header: main.py # Training loop for epoch in range(1000): train_loss, train_acc = processData(train_data) if epoch % 100 == 99: print('--- Epoch %d' % (epoch + 1)) print('Train:\tLoss %.3f | Accuracy: %.3f' % (train_loss, train_acc)) test_loss, test_acc = processData(test_data, backprop=False) print('Test:\tLoss %.3f | Accuracy: %.3f' % (test_loss, test_acc))运行main.py应该输出如下内容：--- Epoch 100 Train: Loss 0.688 | Accuracy: 0.517 Test: Loss 0.700 | Accuracy: 0.500 --- Epoch 200 Train: Loss 0.680 | Accuracy: 0.552 Test: Loss 0.717 | Accuracy: 0.450 --- Epoch 300 Train: Loss 0.593 | Accuracy: 0.655 Test: Loss 0.657 | Accuracy: 0.650 --- Epoch 400 Train: Loss 0.401 | Accuracy: 0.810 Test: Loss 0.689 | Accuracy: 0.650 --- Epoch 500 Train: Loss 0.312 | Accuracy: 0.862 Test: Loss 0.693 | Accuracy: 0.550 --- Epoch 600 Train: Loss 0.148 | Accuracy: 0.914 Test: Loss 0.404 | Accuracy: 0.800 --- Epoch 700 Train: Loss 0.008 | Accuracy: 1.000 Test: Loss 0.016 | Accuracy: 1.000 --- Epoch 800 Train: Loss 0.004 | Accuracy: 1.000 Test: Loss 0.007 | Accuracy: 1.000 --- Epoch 900 Train: Loss 0.002 | Accuracy: 1.000 Test: Loss 0.004 | Accuracy: 1.000 --- Epoch 1000 Train: Loss 0.002 | Accuracy: 1.000 Test: Loss 0.003 | Accuracy: 1.000看起来不错！是否想亲自尝试或修改此代码？在浏览器中运行此RNN。在Github上也可获得这段代码。尾声在这篇文章中，我们介绍了基础的递归神经网络，包括它们是什么，它们如何工作，它们为什么有用，如何训练它们以及如何代码实现。您可以更深入地学习：了解长短期记忆网络（ Long short-term memory）, 一个更强大更流行的RNN框架，或者了解门控递归单元 ( Gated Recurrent Units，GRUs), （LSTM的一个著名变体）的信息。尝试使用ML库，例如 Tensorflow，Keras，或PyTorch。阅读有关双向RNN（Bidirectional RNNs）的信息，该RNN可以处理前向和后向序列，以输出层可以获得更多信息尝试字嵌入（Word Embeddings）如GloVe或Word2Vec，其可以用于将字符转换为转换为更有用的矢量形式。查看自然语言工具包（Natural Language Toolkit，NLTK），这是一个流行的Python库，用于处理人类语言数据。
原文地址在本文中，我们将深入探讨卷积神经网络（CNN）：如何训练CNN（包括梯度推导），从头实现反向传播（仅使用numpy）以及最终构建完整的训练！本文假定你对CNNs有基础了解，如果否，请参考CNN01本部分同时也要求你有多元微分的知识. 您可以根据需要跳过这些部分，但还是建议阅读它们，对写代码有用。开始我们从上一篇文章停下来的地方开始，我们正在使用CNN来解决MNIST手写数字识别问题： 来自MNIST数据集的样本图像我们（简单的）CNN由一个卷机层（Conv layer），一个最大池化层（Max Pooling layer）和一个Softmax层组成。下面是一个简单的示意图： Our (simple) CNN consisted of a Conv layer, a Max Pooling layer, and a Softmax layer. Here's that diagram of our CNN again:我们写了三个类, 分别是这三个层： Conv3x3, MaxPool, 和 Softmax. 每个类都有一个 forward() ，一种用于构建CNN前向传递的方法# Header: cnn.py conv = Conv3x3(8) # 28x28x1 -> 26x26x8 pool = MaxPool2() # 26x26x8 -> 13x13x8 softmax = Softmax(13 * 13 * 8, 10) # 13x13x8 -> 10 def forward(image, label): ''' Completes a forward pass of the CNN and calculates the accuracy and cross-entropy loss. - image is a 2d numpy array - label is a digit ''' # We transform the image from [0, 255] to [-0.5, 0.5] to make it easier # to work with. This is standard practice. out = conv.forward((image / 255) - 0.5) out = pool.forward(out) out = softmax.forward(out) # Calculate cross-entropy loss and accuracy. np.log() is the natural log. loss = -np.log(out[label]) acc = 1 if np.argmax(out) == label else 0 return out, loss, acc你可以阅读代码或 在浏览器中运行. 也可以在Github上找到它.下面是CNN的输出：MNIST CNN initialized! [Step 100] Past 100 steps: Average Loss 2.302 | Accuracy: 11% [Step 200] Past 100 steps: Average Loss 2.302 | Accuracy: 8% [Step 300] Past 100 steps: Average Loss 2.302 | Accuracy: 3% [Step 400] Past 100 steps: Average Loss 2.302 | Accuracy: 12%显然，我们希望能有10%以上的准确度……让我们训练CNN。训练概述训练神经网络通常包括两个阶段：一个前向阶段，其中输入完全通过神经网络。一个后向阶段，其中梯度被反向传播（backpropagated or backprop)，而且权重不断更新。我们将遵循这种模式来训练我们的CNN。我们还将使用两个主要特定于实现的想法：在前向阶段，每一层将缓存（cashe）任何后向阶段所需的任何数据（例如输入，中间值等）。这意味着任何后向阶段都必须在相应的前向阶段之前。在后向阶段，每一层将接收一个梯度（gradient）并且还返回一个梯度。它将获得相对于其输出的损失梯度 ($\frac{\partial L}{\partial \text{out}}$) ，并返回相对于其输入的损失梯度（$\frac{\partial L}{\partial \text{in}}$​).这两个想法将有助于保持我们训练的整洁有序。了解原因的最好方法可能是查看代码：# Feed forward out = conv.forward((image / 255) - 0.5) out = pool.forward(out) out = softmax.forward(out) # Calculate initial gradient gradient = np.zeros(10) # ... # Backprop gradient = softmax.backprop(gradient) gradient = pool.backprop(gradient) gradient = conv.backprop(gradient)看，我们的代码是不是干净多了？现在想象一下，建立一个由50层而不是3层组成的网络-他甚至比良好的系统更有价值反向传播（Backprop）: Softmax首先，回顾一下交叉熵损失（cross-entropy loss）：$$ L = -\ln(p_c) $$其中 $p_c$是正确组c（换句话说，我们当前图像的实际数字是多少）的预测概率。需要更多解释？回顾前一篇文章的Cross-Entropy Loss 。我们需要计算的第一件事是反向阶段Softmax层的输入， $\frac{\partial L}{\partial out_s}$，其中$out_s$是Softmax层的输出：10个概率组成的向量。这很容易，因为只有$p_i$出现在损耗方程中：$$ \frac{\partial L}{\partial out_s(i)} = \begin{cases} 0 & \text{if $i \neq c$} \\ -\frac{1}{p_i} & \text{if $i = c$} \\ \end{cases} $$提醒：c是正确的组。这就是您看到的初始梯度。# Calculate initial gradient gradient = np.zeros(10) gradient[label] = -1 / out[label]我们几乎已经准备好实现我们的第一个反向阶段-只需要首先执行前面讨论的前向阶段缓存即可：# Header: softmax.py class Softmax: # ... def forward(self, input): ''' Performs a forward pass of the softmax layer using the given input. Returns a 1d numpy array containing the respective probability values. - input can be any array with any dimensions. ''' self.last_input_shape = input.shape # highlight-line input = input.flatten() self.last_input = input # highlight-line input_len, nodes = self.weights.shape totals = np.dot(input, self.weights) + self.biases self.last_totals = totals # highlight-line exp = np.exp(totals) return exp / np.sum(exp, axis=0)我们在此处缓存3件事，这对于实现后向阶段非常有用：降维（ flatten）之前，input的形状（shape）。降维之后的input。总数，这是给SOFTMAX激活函数传的值。有了这些，我们就可以开始推导反向传播阶段的梯度。我们已经得到了Softmax反向阶段的梯度：$\frac{\partial L}{\partial out_s}$​。有这样一个事实： $\frac{\partial L}{\partial out_s}$只有一个非零项c，即正确组。这意味着我们可以忽略一切，除了 $out_s(c)$！首先，计算$out_s(c)$（传递给softmax激活函数的值）相对于总数的的梯度。令第i组的总数为$t_i$ ，然后 $out_s(c)$ 可以表示为:$$ out_s(c) = \frac{e^{t_c}}{\sum_i e^{t_i}} = \frac{e^{t_c}}{S} $$其中 $S = \sum_i e^{t_i}$.需要复习Softmax？阅读我的 Softmax简易介绍.然后考虑第k组（$k \neq c$）. $out_s(c)$ 可以表示为:$$ out_s(c) = e^{t_c} S^{-1} $$使用链式法则求导$$ \begin{aligned} \frac{\partial out_s(c)}{\partial t_k} &= \frac{\partial out_s(c)}{\partial S} (\frac{\partial S}{\partial t_k}) \\ &= -e^{t_c} S^{-2} (\frac{\partial S}{\partial t_k}) \\ &= -e^{t_c} S^{-2} (e^{t_k}) \\ &= \boxed{\frac{-e^{t_c} e^{t_k}}{S^2}} \\ \end{aligned} $$记住，这里假设 $k \neq c$. 现在我们同样可以对c求导, 这次要使用 除法法则 (因为$e^{t_c}$在 $out_s(c)$的分子上):$$ \begin{aligned} \frac{\partial out_s(c)}{\partial t_c} &= \frac{S e^{t_c} - e^{t_c} \frac{\partial S}{\partial t_c}}{S^2} \\ &= \frac{Se^{t_c} - e^{t_c}e^{t_c}}{S^2} \\ &= \boxed{\frac{e^{t_c} (S - e^{t_c})}{S^2}} \\ \end{aligned} $$上面是本文最难的部分，接下来会变得简单。相应代码：# Header: softmax.py class Softmax: # ... def backprop(self, d_L_d_out): ''' Performs a backward pass of the softmax layer. Returns the loss gradient for this layer's inputs. - d_L_d_out is the loss gradient for this layer's outputs. ''' # We know only 1 element of d_L_d_out will be nonzero for i, gradient in enumerate(d_L_d_out): if gradient == 0: continue # e^totals t_exp = np.exp(self.last_totals) # Sum of all e^totals S = np.sum(t_exp) # Gradients of out[i] against totals d_out_d_t = -t_exp[i] * t_exp / (S ** 2) d_out_d_t[i] = t_exp[i] * (S - t_exp[i]) / (S ** 2) # ... to be continued记住 $\frac{\partial L}{\partial out_s}$ 只有正确组才是唯一的非零项。 We start by looking for $c$ by looking for a nonzero gradient in 我们开始通过在d_L_d_out寻找非零梯度来找到这个c. 一旦我们找到了，就可以使用上面的结果来计算梯度$\frac{\partial out_s(i)}{\partial t}$ (d_out_d_totals)：$$ \frac{\partial out_s(k)}{\partial t} = \begin{cases} \frac{-e^{t_c} e^{t_k}}{S^2} & \text{if $k \neq c$} \\ \frac{e^{t_c} (S - e^{t_c})}{S^2} & \text{if $k = c$} \\ \end{cases} $$最终我们希望得到一个关于权重、偏置和输入的损失梯度：我们用权重梯度$\frac{\partial L}{\partial w}$, 更新权重用偏置梯度, $\frac{\partial L}{\partial b}$, 更新偏置.从方法backprop() 返回输入梯度, $\frac{\partial L}{\partial input}$,以便下一层可以使用它。这就是我们在概述一节中所讲的返回梯度！为了计算这三个损失梯度，我们首先需要得到另外三个结果：相对于权重、偏置和输入的总和（total）梯度。相关方程式如下：$$ t = w * input + b $$$$ \frac{\partial t}{\partial w} = input $$$$ \frac{\partial t}{\partial b} = 1 $$$$ \frac{\partial t}{\partial input} = w $$将所有内容放到一起：$$ \frac{\partial L}{\partial w} = \frac{\partial L}{\partial out} * \frac{\partial out}{\partial t} * \frac{\partial t}{\partial w} $$$$ \frac{\partial L}{\partial b} = \frac{\partial L}{\partial out} * \frac{\partial out}{\partial t} * \frac{\partial t}{\partial b} $$$$ \frac{\partial L}{\partial input} = \frac{\partial L}{\partial out} * \frac{\partial out}{\partial t} * \frac{\partial t}{\partial input} $$代码实现：# Header: softmax.py class Softmax: # ... def backprop(self, d_L_d_out): ''' Performs a backward pass of the softmax layer. Returns the loss gradient for this layer's inputs. - d_L_d_out is the loss gradient for this layer's outputs. ''' # We know only 1 element of d_L_d_out will be nonzero for i, gradient in enumerate(d_L_d_out): if gradient == 0: continue # e^totals t_exp = np.exp(self.last_totals) # Sum of all e^totals S = np.sum(t_exp) # Gradients of out[i] against totals d_out_d_t = -t_exp[i] * t_exp / (S ** 2) d_out_d_t[i] = t_exp[i] * (S - t_exp[i]) / (S ** 2) # highlight-start # Gradients of totals against weights/biases/input d_t_d_w = self.last_input d_t_d_b = 1 d_t_d_inputs = self.weights # Gradients of loss against totals d_L_d_t = gradient * d_out_d_t # Gradients of loss against weights/biases/input d_L_d_w = d_t_d_w[np.newaxis].T @ d_L_d_t[np.newaxis] d_L_d_b = d_L_d_t * d_t_d_b d_L_d_inputs = d_t_d_inputs @ d_L_d_t # highlight-end # ... to be continued首先，我们预先计算d_L_d_t 因为它将被多次使用。然后，我们计算每个梯度：d_L_d_w：我们需要二维数组做矩阵乘法（@），但d_t_d_w并d_L_d_t是一维数组。np.newaxis使我们可以轻松地创建长度为1的新轴，因此最终将 (input_len, 1) 位矩阵与 (1, nodes)维矩阵相乘。因此，的最终结果d_L_d_w的形状是(input_len, nodes)，与self.weights形状相同！d_L_d_b：因为d_t_d_b是1 ，所以这很简单。d_L_d_inputs：我们将(input_len, nodes) 维矩阵与 (nodes, 1) 维矩阵相乘，得到长度input_len的结果。上述计算例子是理解为什么代码长这样的最好方式。计算完所有梯度后，剩下的就是实际训练Softmax层！就像我们在介绍神经网络时所做的那样，我们将使用随机梯度下降（SGD）更新权重和偏置，然后返回d_L_d_inputs：# Header: softmax.py class Softmax # ... def backprop(self, d_L_d_out, learn_rate): # highlight-line ''' Performs a backward pass of the softmax layer. Returns the loss gradient for this layer's inputs. - d_L_d_out is the loss gradient for this layer's outputs. - learn_rate is a float # highlight-line ''' # We know only 1 element of d_L_d_out will be nonzero for i, gradient in enumerate(d_L_d_out): if gradient == 0: continue # e^totals t_exp = np.exp(self.last_totals) # Sum of all e^totals S = np.sum(t_exp) # Gradients of out[i] against totals d_out_d_t = -t_exp[i] * t_exp / (S ** 2) d_out_d_t[i] = t_exp[i] * (S - t_exp[i]) / (S ** 2) # Gradients of totals against weights/biases/input d_t_d_w = self.last_input d_t_d_b = 1 d_t_d_inputs = self.weights # Gradients of loss against totals d_L_d_t = gradient * d_out_d_t # Gradients of loss against weights/biases/input d_L_d_w = d_t_d_w[np.newaxis].T @ d_L_d_t[np.newaxis] d_L_d_b = d_L_d_t * d_t_d_b d_L_d_inputs = d_t_d_inputs @ d_L_d_t # highlight-start # Update weights / biases self.weights -= learn_rate * d_L_d_w self.biases -= learn_rate * d_L_d_b return d_L_d_inputs.reshape(self.last_input_shape) # highlight-end注意，我们添加了一个learn_rate参数来控制我们更新权重的速度。另外，我们必须在返回d_L_d_inputs 之间进行reshape()操作，因为在前向传递过程中我们将输入降维了：# Header: softmax.py class Softmax: # ... def forward(self, input): ''' Performs a forward pass of the softmax layer using the given input. Returns a 1d numpy array containing the respective probability values. - input can be any array with any dimensions. ''' self.last_input_shape = input.shape input = input.flatten() # highlight-line self.last_input = input # ...改变 last_input_shape 的形状以使该层为输入返回的梯度具有与原始给定的输入一样的格式。测试：Softmax Backprop我们已经完成了第一个反向传播方案！让我们快速地进行测试。我们将应用train()方法到前一篇文章的代码程序# Header: cnn.py # Imports and setup here # ... def forward(image, label): # Implementation excluded # ... def train(im, label, lr=.005): ''' Completes a full training step on the given image and label. Returns the cross-entropy loss and accuracy. - image is a 2d numpy array - label is a digit - lr is the learning rate ''' # Forward out, loss, acc = forward(im, label) # Calculate initial gradient gradient = np.zeros(10) gradient[label] = -1 / out[label] # Backprop gradient = softmax.backprop(gradient, lr) # TODO: backprop MaxPool2 layer # TODO: backprop Conv3x3 layer return loss, acc print('MNIST CNN initialized!') # Train! loss = 0 num_correct = 0 for i, (im, label) in enumerate(zip(train_images, train_labels)): if i % 100 == 99: print( '[Step %d] Past 100 steps: Average Loss %.3f | Accuracy: %d%%' % (i + 1, loss / 100, num_correct) ) loss = 0 num_correct = 0 l, acc = train(im, label) loss += l num_correct += acc结果将类似于MNIST CNN initialized! [Step 100] Past 100 steps: Average Loss 2.239 | Accuracy: 18% [Step 200] Past 100 steps: Average Loss 2.140 | Accuracy: 32% [Step 300] Past 100 steps: Average Loss 1.998 | Accuracy: 48% [Step 400] Past 100 steps: Average Loss 1.861 | Accuracy: 59% [Step 500] Past 100 steps: Average Loss 1.789 | Accuracy: 56% [Step 600] Past 100 steps: Average Loss 1.809 | Accuracy: 48% [Step 700] Past 100 steps: Average Loss 1.718 | Accuracy: 63% [Step 800] Past 100 steps: Average Loss 1.588 | Accuracy: 69% [Step 900] Past 100 steps: Average Loss 1.509 | Accuracy: 71% [Step 1000] Past 100 steps: Average Loss 1.481 | Accuracy: 70%损失在减少，准确性在上升，CNN已经开始学习了！Backprop: 最大池化不能训练最大池化层，因为它实际上没有任何权重，但是我们仍然需要实现一个backprop()方法来计算梯度。我们将再次从添加前向阶段缓存开始。这次需要缓存的只是输入：# Header: maxpool.py class MaxPool2: # ... def forward(self, input): ''' Performs a forward pass of the maxpool layer using the given input. Returns a 3d numpy array with dimensions (h / 2, w / 2, num_filters). - input is a 3d numpy array with dimensions (h, w, num_filters) ''' self.last_input = input #highlight-line # More implementation # ...在前向传递过程中，“最大池化”（Max Pooling）层将获取一个立体（三维）输入，并通过选择2x2块上的最大值将其宽度和高度尺寸减半。后向传递则相反：通过将每个梯度值分配给其相应2x2块中原始最大值所在的位置，我们将损失梯度的宽度和高度加倍。这是一个例子。最大池化层的前向阶段： 将4x4输入转换为2x2输出的前向阶段示例后向阶段如下： 向后阶段将2x2梯度转换为4x4梯度的示例每个梯度值都分配给原始最大值所在的位置，其他每个值均为零。为什么Max Pooling层的后向阶段这样起作用？从直觉上考虑一下$\frac{\partial L}{\partial inputs}$。如果输入像素不是其2x2块中的最大值，则对损失的边际影响为零，因为稍微更改该值根本不会改变输出！换一种说法，非最大像素的$\frac{\partial L}{\partial input} = 0$。另一方面，该输入像素是最大值会使它的值传递给输出，所以 $\frac{\partial output}{\partial input} = 1$，意味着$\frac{\partial L}{\partial input} = \frac{\partial L}{\partial output}$。我们可以使用在第1部分中编写的iterate_regions()方法非常快地实现这一点：# Header: maxpool.py class MaxPool2: # ... def iterate_regions(self, image): ''' Generates non-overlapping 2x2 image regions to pool over. - image is a 2d numpy array ''' h, w, _ = image.shape new_h = h // 2 new_w = w // 2 for i in range(new_h): for j in range(new_w): im_region = image[(i * 2):(i * 2 + 2), (j * 2):(j * 2 + 2)] yield im_region, i, j def backprop(self, d_L_d_out): ''' Performs a backward pass of the maxpool layer. Returns the loss gradient for this layer's inputs. - d_L_d_out is the loss gradient for this layer's outputs. ''' d_L_d_input = np.zeros(self.last_input.shape) for im_region, i, j in self.iterate_regions(self.last_input): h, w, f = im_region.shape amax = np.amax(im_region, axis=(0, 1)) for i2 in range(h): for j2 in range(w): for f2 in range(f): # If this pixel was the max value, copy the gradient to it. if im_region[i2, j2, f2] == amax[f2]: d_L_d_input[i * 2 + i2, j * 2 + j2, f2] = d_L_d_out[i, j, f2] return d_L_d_input对于每个卷积核中每个2x2图像区域中的每个像素，如果是前向传递的最大值，就复制d_L_d_out到d_L_d_input 接下来进入我们最后一层反向传播: Conv通过Conv层进行反向传播是训练CNN的核心。# Header: conv.py class Conv3x3 # ... def forward(self, input): ''' Performs a forward pass of the conv layer using the given input. Returns a 3d numpy array with dimensions (h, w, num_filters). - input is a 2d numpy array ''' self.last_input = input # highlight-line # More implementation # ...提醒一下：为简单起见，我们假设转换层的输入是二维数组。这仅对本例有效，因为我们将其用作网络的第一层。如果我们要建立一个需要多次使用Conv3x3的更大的网络，则必须将输入设为三维数组。我们主要对conv层中卷积核的损耗梯度感兴趣，因为我们需要用它来更新卷积核的权重。对于conv层，我们已经有$\frac{\partial L}{\partial out}$，所以我们只需要 $\frac{\partial out}{\partial filters}$​。要计算这一点，我们要问自己：改变卷积核的权重将如何影响conv层的输出？现实情况是，更改任何卷积核权重都会影响该滤波器的整个输出图像，因为在卷积过程中每个输出像素都会使用每个像素权重。为了更容易明白，让我们一次只考虑一个输出像素：修改滤镜将如何改变一个特定输出像素的结果？这是一个超级简单的示例，可以帮助您思考以下问题： 3x3图像（左）与3x3卷积核（中间）卷积后产生1x1输出（右）如果我们将中间卷积核的权重增加为1，会怎么样？输出值将会是80 类似地，将任何位置的卷积核权重增加1将使输出增加相应图像像素的值！这表明特定输出像素相对于特定卷积核权重的导数就是相应的图像像素值。进行数学运算可以验证这一点：$$ \begin{aligned} \text{out(i, j)} &= \text{convolve(image, filter)} \\ &= \sum_{x=0}^3 \sum_{y=0}^3 \text{image}(i + x, j + y) * \text{filter}(x, y) \\ \end{aligned} $$$$ \frac{\partial \text{out}(i, j)}{\partial \text{filter}(x, y)} = \text{image}(i + x, j + y) $$我们可以将它们放在一起以找到特定卷积核权重的损耗梯度：$$ \begin{aligned} \frac{\partial L}{\partial \text{filter}(x, y)} &= \sum_i \sum_j \frac{\partial L}{\partial \text{out}(i, j)} * \frac{\partial \text{out}(i, j)}{\partial \text{filter}(x, y)} \end{aligned} $$准备为我们的conv层实现backprop！# Header: conv.py class Conv3x3 # ... def backprop(self, d_L_d_out, learn_rate): ''' Performs a backward pass of the conv layer. - d_L_d_out is the loss gradient for this layer's outputs. - learn_rate is a float. ''' d_L_d_filters = np.zeros(self.filters.shape) for im_region, i, j in self.iterate_regions(self.last_input): for f in range(self.num_filters): d_L_d_filters[f] += d_L_d_out[i, j, f] * im_region # Update filters self.filters -= learn_rate * d_L_d_filters # We aren't returning anything here since we use Conv3x3 as # the first layer in our CNN. Otherwise, we'd need to return # the loss gradient for this layer's inputs, just like every # other layer in our CNN. return None我们通过迭代每个图像区域/卷积核并逐步建立损耗梯度来应用导出的方程式。涵盖所有内容后，我们将像以前一样使用SGD 更新self.filters。请注意解释为什么我们返回一个None-输入的损耗梯度的推导与我们刚才做的很相似，留给读者作为练习：)。这样就完成了！该测试一下了……训练CNN我们将对CNN进行一些训练，在训练期间跟踪其进度，然后在单独的测试集上对其进行测试。这是完整的代码：# Header: cnn.py import mnist import numpy as np from conv import Conv3x3 from maxpool import MaxPool2 from softmax import Softmax # We only use the first 1k examples of each set in the interest of time. # Feel free to change this if you want. train_images = mnist.train_images()[:1000] train_labels = mnist.train_labels()[:1000] test_images = mnist.test_images()[:1000] test_labels = mnist.test_labels()[:1000] conv = Conv3x3(8) # 28x28x1 -> 26x26x8 pool = MaxPool2() # 26x26x8 -> 13x13x8 softmax = Softmax(13 * 13 * 8, 10) # 13x13x8 -> 10 def forward(image, label): ''' Completes a forward pass of the CNN and calculates the accuracy and cross-entropy loss. - image is a 2d numpy array - label is a digit ''' # We transform the image from [0, 255] to [-0.5, 0.5] to make it easier # to work with. This is standard practice. out = conv.forward((image / 255) - 0.5) out = pool.forward(out) out = softmax.forward(out) # Calculate cross-entropy loss and accuracy. np.log() is the natural log. loss = -np.log(out[label]) acc = 1 if np.argmax(out) == label else 0 return out, loss, acc def train(im, label, lr=.005): ''' Completes a full training step on the given image and label. Returns the cross-entropy loss and accuracy. - image is a 2d numpy array - label is a digit - lr is the learning rate ''' # Forward out, loss, acc = forward(im, label) # Calculate initial gradient gradient = np.zeros(10) gradient[label] = -1 / out[label] # Backprop gradient = softmax.backprop(gradient, lr) gradient = pool.backprop(gradient) gradient = conv.backprop(gradient, lr) return loss, acc print('MNIST CNN initialized!') # Train the CNN for 3 epochs for epoch in range(3): print('--- Epoch %d ---' % (epoch + 1)) # Shuffle the training data permutation = np.random.permutation(len(train_images)) train_images = train_images[permutation] train_labels = train_labels[permutation] # Train! loss = 0 num_correct = 0 for i, (im, label) in enumerate(zip(train_images, train_labels)): if i > 0 and i % 100 == 99: print( '[Step %d] Past 100 steps: Average Loss %.3f | Accuracy: %d%%' % (i + 1, loss / 100, num_correct) ) loss = 0 num_correct = 0 l, acc = train(im, label) loss += l num_correct += acc # Test the CNN print('\n--- Testing the CNN ---') loss = 0 num_correct = 0 for im, label in zip(test_images, test_labels): _, l, acc = forward(im, label) loss += l num_correct += acc num_tests = len(test_images) print('Test Loss:', loss / num_tests) print('Test Accuracy:', num_correct / num_tests)Example output from running the code:MNIST CNN initialized! --- Epoch 1 --- [Step 100] Past 100 steps: Average Loss 2.254 | Accuracy: 18% [Step 200] Past 100 steps: Average Loss 2.167 | Accuracy: 30% [Step 300] Past 100 steps: Average Loss 1.676 | Accuracy: 52% [Step 400] Past 100 steps: Average Loss 1.212 | Accuracy: 63% [Step 500] Past 100 steps: Average Loss 0.949 | Accuracy: 72% [Step 600] Past 100 steps: Average Loss 0.848 | Accuracy: 74% [Step 700] Past 100 steps: Average Loss 0.954 | Accuracy: 68% [Step 800] Past 100 steps: Average Loss 0.671 | Accuracy: 81% [Step 900] Past 100 steps: Average Loss 0.923 | Accuracy: 67% [Step 1000] Past 100 steps: Average Loss 0.571 | Accuracy: 83% --- Epoch 2 --- [Step 100] Past 100 steps: Average Loss 0.447 | Accuracy: 89% [Step 200] Past 100 steps: Average Loss 0.401 | Accuracy: 86% [Step 300] Past 100 steps: Average Loss 0.608 | Accuracy: 81% [Step 400] Past 100 steps: Average Loss 0.511 | Accuracy: 83% [Step 500] Past 100 steps: Average Loss 0.584 | Accuracy: 89% [Step 600] Past 100 steps: Average Loss 0.782 | Accuracy: 72% [Step 700] Past 100 steps: Average Loss 0.397 | Accuracy: 84% [Step 800] Past 100 steps: Average Loss 0.560 | Accuracy: 80% [Step 900] Past 100 steps: Average Loss 0.356 | Accuracy: 92% [Step 1000] Past 100 steps: Average Loss 0.576 | Accuracy: 85% --- Epoch 3 --- [Step 100] Past 100 steps: Average Loss 0.367 | Accuracy: 89% [Step 200] Past 100 steps: Average Loss 0.370 | Accuracy: 89% [Step 300] Past 100 steps: Average Loss 0.464 | Accuracy: 84% [Step 400] Past 100 steps: Average Loss 0.254 | Accuracy: 95% [Step 500] Past 100 steps: Average Loss 0.366 | Accuracy: 89% [Step 600] Past 100 steps: Average Loss 0.493 | Accuracy: 89% [Step 700] Past 100 steps: Average Loss 0.390 | Accuracy: 91% [Step 800] Past 100 steps: Average Loss 0.459 | Accuracy: 87% [Step 900] Past 100 steps: Average Loss 0.316 | Accuracy: 92% [Step 1000] Past 100 steps: Average Loss 0.460 | Accuracy: 87% --- Testing the CNN --- Test Loss: 0.5979384893783474 Test Accuracy: 0.78我们的代码有效！在仅有3000个训练步骤中，我们从损失为2.3且准确度为10％的模型变为损失为0.6且准确度为78％的模型。是否想亲自尝试或修改此代码？在浏览器中运行此CNN。在Github上也可以找到该代码。为了节省时间，在此示例中，我们仅使用了整个MNIST数据集的一个子集-CNN的实现并不是特别快。如果我们想训练MNIST CNN使其更加有效，我们会使用ML库如Keras。为了说明我们的CNN的功能，我使用Keras训练了和本文完全相同的 的CNN：# Header: cnn_keras.py import numpy as np import mnist from keras.models import Sequential from keras.layers import Conv2D, MaxPooling2D, Dense, Flatten from keras.utils import to_categorical from keras.optimizers import SGD train_images = mnist.train_images() train_labels = mnist.train_labels() test_images = mnist.test_images() test_labels = mnist.test_labels() train_images = (train_images / 255) - 0.5 test_images = (test_images / 255) - 0.5 train_images = np.expand_dims(train_images, axis=3) test_images = np.expand_dims(test_images, axis=3) model = Sequential([ Conv2D(8, 3, input_shape=(28, 28, 1), use_bias=False), MaxPooling2D(pool_size=2), Flatten(), Dense(10, activation='softmax'), ]) model.compile(SGD(lr=.005), loss='categorical_crossentropy', metrics=['accuracy']) model.fit( train_images, to_categorical(train_labels), batch_size=1, epochs=3, validation_data=(test_images, to_categorical(test_labels)), )在完整的 MNIST数据集（60k训练图像）上运行该代码将得到如下结果：Epoch 1 loss: 0.2433 - acc: 0.9276 - val_loss: 0.1176 - val_acc: 0.9634 Epoch 2 loss: 0.1184 - acc: 0.9648 - val_loss: 0.0936 - val_acc: 0.9721 Epoch 3 loss: 0.0930 - acc: 0.9721 - val_loss: 0.0778 - val_acc: 0.9744通过这个简单的CNN，我们可以达到97.4％的测试准确性！有了更好的CNN架构，我们可以进一步改善-在这个官方的Keras MNIST CNN示例中，它们在12个周期后达到了99.25％的准确度。不熟悉Keras？阅读有关使用Keras构建第一个神经网络或使用Keras训练CNN实现CNN的教程。本文所有代码均可在 Github上找到.
原文链接在过去的几年里，关于卷积神经网络（Convolution Neural Networks，CNN）的讨论越来越多，特别是因为它彻底改变了计算机视觉领域。在本文中，我们将基于神经网络的基本背景知识，探索CNN是什么，了解它们的工作原理，并在Python中从头开始构建一个真正的CNN（仅使用numpy）。这篇文章仅假设你拥有神经网络的基本知识。我对神经网络的介绍涵盖了您需要了解的所有内容，因此您可能需要首先阅读它。为什么使用CNNCNN的经典案例是图像分类，例如查看宠物的图像并确定它是猫还是狗。那么问题来了，为什么不使用普通的神经网络？好问题原因1：图片很大如今，有关计算机视觉问题的图像通常为224x224或更大。想象一下，建立一个神经网络来处理224x224彩色图像：包括图像中的3个彩色通道（RGB），也就是说需要224 x 224 x 3 = 150528个输入特征！在这种网络中，典型的隐藏层可能有1024个节点，因此仅第一层就必须训练150,528 x 1024 = 150+百万个权重。我们的网络非常庞大，几乎无法训练。而且我们也不需要那么多的权重。我们知道像素在与其相邻的环境中最有用。图像中的对象由小的局部特征组成，例如眼睛的圆形虹膜或一张纸的方形角。第一个隐藏层中的每个节点查看每个像素是否很浪费？原因2：位置会发生变化如果你训练了一个网络来检测狗，你会希望它能够检测狗，而不管它出现在图像的什么地方。想象一下，训练一个网络，它能很好地处理一个特定的狗的图像，但是给它一个稍微改变了的图像。狗不会激活相同的神经元，所以网络的反应会完全不同！我们很快就会看到CNN如何帮助我们解决这些问题。数据集在本文中，我们将解决计算机视觉的“Hello, World!”：MNIST手写数字分类问题。很简单：给定图像，将其分类为数字。 MNIST数据集MNIST数据集中的每个图像均为28x28像素，并包含居中的灰度数字。说实话，一般的神经网络实际上可以很好地解决这个问题。您可以将每张图像视为28 x 28 = 784维向量，将其输入到784维输入层，堆叠一些隐藏层，最后得到10个节点的输出层，每个数字1个节点。但是它只会在MNIST数据集包含居中的小图像时起作用，本例中我们不会遇到上述尺寸或偏移问题。请记住，大多数现实世界中的图像分类问题并非易事。卷积什么是卷积神经网络？它是使用卷积层（又称为Conv层）的神经网络，其基于卷积的数学运算。Conv层由一组filter（卷积核）组成，您可以将其视为二维的数字矩阵。这是一个3x3卷积核示例： 我们可以输入图像和卷积核，将卷积核与输入图像卷积来生成输出图像。这包括在某些位置将卷积核覆盖在图像顶部。在卷积核中的值和图像中相应的值之间执行逐元素乘法（element-wise multiplication）。加总所有逐元素乘法的结果。和是输出图像中目标像素的输出值。在所有位置重复该过程。旁注：从技术上讲，我们（以及许多CNN）实际上在这里使用互相关而不是卷积，但是它们几乎是同一件事。我不会在这篇文章中介绍差异，因为它并不重要，但是如果您感到好奇，可以随时查找相关资料。这个四步描述有点抽象，所以让我们做一个例子。考虑一下这个微小的4x4灰度图像和3x3卷积核： 4x4图像（左）和3x3卷积核（右）图像中的数字表示像素强度，其中0为黑色，255为白色。我们将对输入图像和卷积核进行卷积以生成2x2的输出图像： 首先，让我们将卷积核覆盖在图像的左上角： 步骤1：将卷积核（右）覆盖在图像（左）的上方接下来，我们在重叠的图像值和卷积核值之间执行逐元素乘法。结果如下，从左到右，从上到下：图像值卷积核值结果0-1050000100-2080003126233-1-339000010步骤2：执行逐元素乘法。接下来，加总所有的结果，这很容易$$ 62 - 33 = \boxed{29} $$最后，我们将结果放置在输出图像的目标像素中。由于我们的卷积核覆盖在输入图像的左上角，因此我们的目标像素是输出图像的左上像素： 重复以上步骤，得到其他位置像素 它有什么用？让我们缩小一下，在更高的层次上看。将一个图像与一个卷积核进行卷积有什么用?我们可以从我们一直使用的3x3卷积核开始，它通常被称为垂直索伯滤波器 垂直索伯滤波器这是一个垂直索伯滤波器的示例： 图像与垂直索伯滤波器卷积类似地，还有一个水平索伯滤波器卷积： 水平索伯滤波器卷积 图像与水平索伯滤波器卷积看看发生了什么事？索伯滤波器是边缘检测算子（edge-detectors）。垂直索伯滤波器检测垂直边缘，而水平索伯滤波器检测水平边缘。现在可以轻松地解释输出图像：输出图像中的亮像素（值较高的像素）表明原始图像周围有很强的边缘。您能看到为什么边缘检测图像比原始图像更有用的原因吗？回想一下我们的MNIST手写数字分类问题。经过MNIST训练的CNN可能会通过使用边缘检测算子，并检查图像中心附近的两个突出的垂直边缘来寻找数字1。通常，卷积可以帮助我们寻找特定的局部图像特征（例如边缘），以便以后在网络中使用。填充（Padding）还记得将4x4输入图像与3x3卷积核进行卷积以生成2x2的输出图像吗？通常，我们希望输出图像的大小与输入图像的大小相同。为此，我们在图像周围添加零，以便可以在更多位置覆盖卷积核。3x3卷积核需要填充1个像素： 4x4输入与3x3卷积核卷积产生4x4输出这被称为“相同”填充，因为输入和输出具有相同的尺寸。不使用任何填充（这是我们一直在做的），有时也称为“有效”填充。卷积层既然我们知道了图像卷积的工作原理以及为什么它有用，那么让我们看看它在CNN中的实际用法。如前所述，CNN包括使用一组卷积核将输入图像转换为输出图像的卷积层。卷积层的主要参数是一系列卷积核。对于我们的MNIST CNN，我们将使用带有8个卷积核的小型卷机层作为网络的初始层。这意味着它将把28x28的平面输入图像变成26x26x8的立体输出图像： 提醒：输出是26x26x8而不是28x28x8，因为我们使用的是有效的padding，它将输入的宽度和高度减少2。卷积层中的8个卷积核中的每一个都产生26x26的输出，因此堆叠在一起就构成了26x26x8的立体。3*3（卷积核的大小）*8（卷积核数量）=72，也就是说我们只需要72个权重！实现卷积我们将实现一个conv层的前馈部分，该部分负责将卷积核与输入图像进行卷积以产生输出。为简单起见，我们假定卷积核始终为3x3（事实并非如此-5x5和7x7卷积核也很常见）。# Header: conv.py import numpy as np class Conv3x3: # A Convolution layer using 3x3 filters. def __init__(self, num_filters): self.num_filters = num_filters # filters is a 3d array with dimensions (num_filters, 3, 3) # We divide by 9 to reduce the variance of our initial values self.filters = np.random.randn(num_filters, 3, 3) / 9Conv3x3类只接受一个参数：卷积核的数量。在init函数中，我们存储卷积核的数量，并使用NumPy的randn()方法初始化一个随机卷积核数组。注意：在初始化过程中，除以9会比您想象的要重要。如果初始值太大或太小，训练网络将无效。要了解更多信息，请阅读Xavier Initialization。接下来，实际的卷积：# Header: conv.py class Conv3x3: # ... def iterate_regions(self, image): ''' Generates all possible 3x3 image regions using valid padding. - image is a 2d numpy array ''' h, w = image.shape for i in range(h - 2): for j in range(w - 2): im_region = image[i:(i + 3), j:(j + 3)] yield im_region, i, j def forward(self, input): ''' Performs a forward pass of the conv layer using the given input. Returns a 3d numpy array with dimensions (h, w, num_filters). - input is a 2d numpy array ''' h, w = input.shape output = np.zeros((h - 2, w - 2, self.num_filters)) for im_region, i, j in self.iterate_regions(input): output[i, j] = np.sum(im_region * self.filters, axis=(1, 2)) # highlight-line return outputpython›iterate_regions()是一种辅助生成器方法，可以为我们生成所有有效的3x3图像区域。 对于该类的后半部分非常有用。上面突出显示了实际执行卷积的代码行。让我们分解一下：im_region, 一个包含相关图像区域的3x3数组.self.filters, 一个三维数组.代码 im_region * self.filters, 利用了numpy的 broadcasting （广播）功能将两个数组逐元素相乘 。其结果是一个三维数组，与 self.filters维度相同.使用np.sum() axis=(1, 2)对上一步得到的结果相加，生成了一个一维数组 num_filters ，其中每个元素包含对应卷积核的卷积结果将结果分配给 output[i, j], 其中包含输出图像中像素的卷积结果对输出中的每个像素执行以上过程，直到获得最终的输出！代码如下：# Header: cnn.py import mnist from conv import Conv3x3 # The mnist package handles the MNIST dataset for us! # Learn more at https://github.com/datapythonista/mnist train_images = mnist.train_images() train_labels = mnist.train_labels() conv = Conv3x3(8) output = conv.forward(train_images[0]) print(output.shape) # (26, 26, 8)到目前为止看起来不错。注意：在我们的 Conv3x3中，为简单起见，我们假设输入为2维numpy数组，因为这就是我们的MNIST图像的存储方式。这对我们有用，因为我们将其用作网络的第一层，但是大多数CNN都具有更多的Conv层。如果我们要建立一个更大的网络，需要多次使用 Conv3x3，则必须使输入为三维numpy数组。池化层（Pooling）图像中的相邻像素往往具有相似的值，因此经过卷积层输出中的相邻像素也有相似的值。也就是卷机层输出中包含了大量多余的信息。例如，如果我们使用边缘检测滤波器，并在某个位置找到了较强的边缘，则很有可能在从原始像素偏移1个像素的位置上也找到了相对较强的边缘。但是，这些都是相同的边缘！我们没有发现任何新东西。池化层解决了这个问题。他们所做的只是减小输入的大小，通过与池化层的值一起输入。该池通常是通过简单的操作，比如max，min或average来实现。这是最大池化层的示例，池化大小为2： 在4x4图像上的最大池化（池大小2）产生2x2输出为了执行最大池化（_max_ pooling），我们以2x2块（因为池大小= 2）遍历输入图像，并将最大值放入对应像素的输出图像中而已！池化将输入的宽度和高度变为除以池大小的宽度和高度。对于我们的MNIST CNN，我们将在初始转换层之后立即放置一个池大小为2的最大池化层。池化层会将26x26x8输入转换为13x13x8输出：Pooling divides the input's width and height by the pool size. For our MNIST CNN, we'll place a Max Pooling layer with a pool size of 2 right after our initial conv layer. The pooling layer will transform a 26x26x8 input into a 13x13x8 output: 池化的代码实现我们将用与上一节中的conv类相同的方法来实现一个 MaxPool2 类：:# Header: maxpool.py import numpy as np class MaxPool2: # A Max Pooling layer using a pool size of 2. def iterate_regions(self, image): ''' Generates non-overlapping 2x2 image regions to pool over. - image is a 2d numpy array ''' h, w, _ = image.shape new_h = h // 2 new_w = w // 2 for i in range(new_h): for j in range(new_w): im_region = image[(i * 2):(i * 2 + 2), (j * 2):(j * 2 + 2)] yield im_region, i, j def forward(self, input): ''' Performs a forward pass of the maxpool layer using the given input. Returns a 3d numpy array with dimensions (h / 2, w / 2, num_filters). - input is a 3d numpy array with dimensions (h, w, num_filters) ''' h, w, num_filters = input.shape output = np.zeros((h // 2, w // 2, num_filters)) for im_region, i, j in self.iterate_regions(input): output[i, j] = np.amax(im_region, axis=(0, 1)) # highlight-line return output该类的工作方式与Conv3x3类似。要从给定图像区域中找到最大值，我们使用numpy的np.amax()方法。我们设定axis=(0, 1)因为我们只想最大化前两个维度，即宽高，并不包括第三个维度：num_filters（卷积核数量）让我们测试一下！# Header: cnn.py import mnist from conv import Conv3x3 from maxpool import MaxPool2 # The mnist package handles the MNIST dataset for us! # Learn more at https://github.com/datapythonista/mnist train_images = mnist.train_images() train_labels = mnist.train_labels() conv = Conv3x3(8) pool = MaxPool2() output = conv.forward(train_images[0]) output = pool.forward(output) print(output.shape) # (13, 13, 8)Softmax要完成我们的CNN，我们需要赋予它实际进行预测的能力。我们将使用标准的最终层解决多类分类问题：Softmax层，这是一个全连接（密集）层，使用Softmax函数作为激活函数。提醒：全连接层将每个节点连接到上一层的每个输出。我们在神经网络入门中就使用了全连接层。如果您以前从未听说过Softmax，请先阅读我对Softmax的快速介绍，然后再继续。使用我们将使用带有10个节点的softmax层作为CNN的最后一层，每个节点代表一个数字。层中的每个节点将连接到每个输入。应用softmax转换后，由节点表示的概率最高的数字将是CNN的输出！ 交叉熵损失（cross-entropy loss）您可能想问，为什么要将输出转换为概率？最高的输出值会不会总是有最高的概率？如果您这样做，那绝对是正确的。我们实际上不需要使用softmax来预测数字 -我们可以选择网络中输出最高的数字！softmax的真正作用是帮助我们量化对预测的置信度，这在训练和评估CNN时很有用。更具体地说，使用softmax让我们能用交叉熵损失，它考虑了我们对每个预测的确定程度。这是我们计算交叉熵损失的方法：$$ L = -\ln(p_c) $$其中c是正确的组 (在本例中，就是正确的数字), $p_c$ 是对c的预测概率, and $\ln$ is the 自然对数. 一般来说, 更低的损失会更好. 例如在完美的情况下，我们有：$$ p_c = 1, L = -\ln(1) = 0 $$在更真实的情况下，我们有$$ p_c = 0.8, L = -\ln(0.8) = 0.223 $$在后续的文章中我们仍会看到交叉熵损失。Softmax代码实现# Header: softmax.py import numpy as np class Softmax: # A standard fully-connected layer with softmax activation. def __init__(self, input_len, nodes): # We divide by input_len to reduce the variance of our initial values self.weights = np.random.randn(input_len, nodes) / input_len self.biases = np.zeros(nodes) def forward(self, input): ''' Performs a forward pass of the softmax layer using the given input. Returns a 1d numpy array containing the respective probability values. - input can be any array with any dimensions. ''' input = input.flatten() input_len, nodes = self.weights.shape totals = np.dot(input, self.weights) + self.biases exp = np.exp(totals) return exp / np.sum(exp, axis=0)这里没有什么太复杂的。一些要点：我们使用flatten() 使其更易使用, 因为我们不再需要它的形状.np.dot() 将 input和 self.weights 逐元素相乘，并将结果加总。.np.exp() 计算用于Softmax的指数。接下来我们把代码放到一起# Header: cnn.py import mnist import numpy as np from conv import Conv3x3 from maxpool import MaxPool2 from softmax import Softmax # We only use the first 1k testing examples (out of 10k total) # in the interest of time. Feel free to change this if you want. test_images = mnist.test_images()[:1000] test_labels = mnist.test_labels()[:1000] conv = Conv3x3(8) # 28x28x1 -> 26x26x8 pool = MaxPool2() # 26x26x8 -> 13x13x8 softmax = Softmax(13 * 13 * 8, 10) # 13x13x8 -> 10 def forward(image, label): ''' Completes a forward pass of the CNN and calculates the accuracy and cross-entropy loss. - image is a 2d numpy array - label is a digit ''' # We transform the image from [0, 255] to [-0.5, 0.5] to make it easier # to work with. This is standard practice. out = conv.forward((image / 255) - 0.5) out = pool.forward(out) out = softmax.forward(out) # Calculate cross-entropy loss and accuracy. np.log() is the natural log. loss = -np.log(out[label]) acc = 1 if np.argmax(out) == label else 0 return out, loss, acc print('MNIST CNN initialized!') loss = 0 num_correct = 0 for i, (im, label) in enumerate(zip(test_images, test_labels)): # Do a forward pass. _, l, acc = forward(im, label) loss += l num_correct += acc # Print stats every 100 steps. if i % 100 == 99: print( '[Step %d] Past 100 steps: Average Loss %.3f | Accuracy: %d%%' % (i + 1, loss / 100, num_correct) ) loss = 0 num_correct = 0运行之后得到以下（类似）的输出MNIST CNN initialized! [Step 100] Past 100 steps: Average Loss 2.302 | Accuracy: 11% [Step 200] Past 100 steps: Average Loss 2.302 | Accuracy: 8% [Step 300] Past 100 steps: Average Loss 2.302 | Accuracy: 3% [Step 400] Past 100 steps: Average Loss 2.302 | Accuracy: 12%这是有道理的：通过初始化随机权重，您可以期望CNN与随机猜测一样好。随机猜测将产生10％的准确性（因为有10个类别），并且交叉熵损失为${-\ln(0.1)} = 2.302$，这就是我们得到的！想亲自运行这段代码? 在浏览器中运行. 同样也可以在 Github上找到这段代码.总结到此为止，CNN的介绍到此结束！在这篇文章中，我们阐述了CNN对于某些问题（例如图像分类）可能更有用的原因。引入了MNIST手写数字数据集。了解了Conv图层，该图层将卷积核与图像进行卷积以产生更多有用的输出。讨论了Pooling层，它可以帮助去除最有用特征之外的所有内容。实现了Softmax层，因此我们可以使用交叉熵损失。我们还没有介绍更多内容，例如如何实际训练CNN。CNN系列的第2部分深入研究了CNN的训练，包括推导梯度和实现反向传播。另外，您还可以学习使用Keras（一个Python深度学习库）实现自己的CNN。如果您急切希望看到训练有素的CNN：示例在MNIST上训练的Keras CNN可以达到99.25％的准确性。
原文地址基础介绍Softmax将任意实数值转换为概率，这在机器学习中通常很有用。其背后的数学非常简单：给定一些数字，将它们作为 e) (一个数学常数) 的幂加总所有以e为底的指数，结果作为分母每个单独指数为分子概率等于分子除以分母更确切地说，Softmax对n个给定的$x_1 \ldots x_n$数字作以下转换 :$$ s(x_i) = \frac{e^{x_i}}{\sum_{j=1}^n e^{x_j}} $$输出始终在$[0, 1]$之间，并且加总为0.因此，它们形成一个概率分布。例子假设我们有数字-1、0、3和5。首先，我们计算分母：$$ \begin{aligned} \text{分母} &= e^{-1} + e^0 + e^3 + e^5 \\ &= \boxed{169.87} \\ \end{aligned} $$然后，我们可以计算分子和概率：$x$分子 ($e^x$)概率 ($\frac{e^x}{169.87}$)-10.3680.002010.006320.090.1185148.410.874x越大，概率越高。另外请注意，所有概率加起来为1.在Python中实现Softmax使用 numpy 实现起来非常容易:import numpy as np def softmax(xs): return np.exp(xs) / sum(np.exp(xs)) xs = np.array([-1, 0, 3, 5]) print(softmax(xs)) # [0.0021657, 0.00588697, 0.11824302, 0.87370431]np.exp() 将xs作为幂，e作为低为什么Softmax如此有用?想象建立一个 神经网络 来回答这个问题: 这张照片是狗还是猫？该神经网络的一种常见的设计是：它输出2个实数，一个表示小狗，另一个表示猫猫，并应用Softmax处理这些值。例如，假设网络输出 $[-1, 2]$:动物$x$$e^x$Probability概率狗-10.3680.047猫27.390.953这意味着我们的网络有95.3％的置信度相信图片是猫。Softmax让我们用概率回答分类问题，比简单的答案（例如，二进制是/否）更有用。
原文地址这可能会让您感到惊讶：神经网络并不复杂！“神经网络”一词经常被用作流行语，但实际上，它们通常比人们想象的要简单得多。这篇文章是针对初学者的，并假定机器学习基础为零。我们在Python中从头实现神经网络。让我们开始吧！构建基础：神经元（Neurons）首先，我们必须引入神经元这个概念，这是神经网络的基本单位。神经元接受输入，并对它们进行一些数学运算，然后输出。下面是双输入神经元的模型： 这里发生了三件事：首先，每个输入都乘以相应的权重：$$\begin{array} { l } { x _ { 1 } \rightarrow x _ { 1 } * w _ { 1 } } \\ { x _ { 2 } \rightarrow x _ { 2 } * w _ { 2 } } \end{array}$$接着，加权输入之和与偏置（bias）b相加：$$\left( x _ { 1 } * w _ { 1 } \right) + \left( x _ { 2 } * w _ { 2 } \right) + b$$最后，总和通过激活函数（activation function）传递：$$y = f \left( x _ { 1 } * w _ { 1 } + x _ { 2 } * w _ { 2 } + b \right)$$激活函数用于将无界输入转换为具有良好的、可预测形式的输出。常用的激活函数是S型函数： 这个S函数将无界的输入（$-\infty ,\infty$）转化成有界的输出（$0,1$），无穷小输出0，无穷大输出1.一个简单的例子假设我们有一个双输入神经元，使用S函数并有以下参数：$$\begin{array} { c } { w = [ 0,1 ] } \\ { b = 4 } \end{array}$$其中，$w = [ 0,1]$仅仅是将w1=0,w2=1写成向量形式。现在，让我们给定神经元一个输入$x=[2,3]$，并用点积的形式更简洁地写公式：$$\begin{aligned} ( w \cdot x ) + b & = \left( \left( w _ { 1 } * x _ { 1 } \right) + \left( w _ { 2 } * x _ { 2 } \right) \right) + b \\ & = 0 * 2 + 1 * 3 + 4 \\ & = 7 \\ y = f ( w \cdot x + b ) & = f ( 7 ) = 0.999 \end{aligned}$$也就是说，给定输入$x=[2,3]$，输出0.999。这种将输入向前传递以获得输出的过程我们称之为前馈（feedforward）神经元的代码实现我们将使用numpy来进行数学运算。import numpy as np def sigmoid(x): # Our activation function: f(x) = 1 / (1 + e^(-x)) return 1 / (1 + np.exp(-x)) class Neuron: def __init__(self, weights, bias): self.weights = weights self.bias = bias def feedforward(self, inputs): # Weight inputs, add bias, then use the activation function total = np.dot(self.weights, inputs) + self.bias return sigmoid(total) weights = np.array([0, 1]) # w1 = 0, w2 = 1 bias = 4 # b = 4 n = Neuron(weights, bias) x = np.array([2, 3]) # x1 = 2, x2 = 3 print(n.feedforward(x)) # 0.9990889488055994同样输出了0.999将神经元组合成神经网络神经网络无非就是一束连接在一起的神经元。一个典型的神经网络如下： 该网络有2个输入，一个包含2个神经元的隐藏层（h1和h2），以及一个带有1个神经元的输出层（o1）。注意o1的输入也就是h1,h2的输出，这也是它被称为神经网络的原因。隐藏层是输入（第一）层和输出（最后一个）层之间的任何层。可以有多个隐藏层！它的工作是将输入转换为输出层可以使用的东西。例子：前馈神经网络使用上图所示的网络，并假设所有神经元的权重相同 w = [0，1]，偏置也相同，b = 0，也有同样的S型激活函数。让h1,h2,o1表示它们所代表的神经元的输出。如果我们输入x=[2,3]会怎么样？$$\begin{aligned} h _ { 1 } = h _ { 2 } & = f ( w \cdot x + b ) \\ & = f ( ( 0 * 2 ) + ( 1 * 3 ) + 0 ) \\ & = f ( 3 ) \\ & = 0.9526 \\ \\ o _ { 1 } = & f \left( w \cdot \left[ h _ { 1 } , h _ { 2 } \right] + b \right) \\ & = f \left( \left( 0 * h _ { 1 } \right) + \left( 1 * h _ { 2 } \right) + 0 \right) \\ & = f ( 0.9526 ) \\ & = [ 0.7216 ] \end{aligned}$$输出是0.7216，非常简单，对吗？神经网络可以具有任何数量的层与任何数量的神经元的。但基本思想保持不变：将输入通过网络中的神经元向前馈送，以获得最终的输出。为了简单起见，我们将在本文的其余部分继续使用上面所示的网络。代码实现import numpy as np # ... code from previous section here class OurNeuralNetwork: ''' A neural network with: - 2 inputs - a hidden layer with 2 neurons (h1, h2) - an output layer with 1 neuron (o1) Each neuron has the same weights and bias: - w = [0, 1] - b = 0 ''' def __init__(self): weights = np.array([0, 1]) bias = 0 # 这里的Neuron类来自上一节的代码 self.h1 = Neuron(weights, bias) self.h2 = Neuron(weights, bias) self.o1 = Neuron(weights, bias) def feedforward(self, x): out_h1 = self.h1.feedforward(x) # 并不是调用的OurNeuralNetwork().feedforward(),而是Neuron().feedforward() out_h2 = self.h2.feedforward(x) # The inputs for o1 are the outputs from h1 and h2 out_o1 = self.o1.feedforward(np.array([out_h1, out_h2])) return out_o1 network = OurNeuralNetwork() x = np.array([2, 3]) print(network.feedforward(x)) # 0.7216325609518421训练神经网络，第一部分假设我们有以下数据：NameWeight (lb)Height (in)GenderAlice13365FBob16072MCharlie15270MDiana12060F让我们训练神经网络，以实现给定身高和体重的输入，来预测性别的效果。 用0代表男性，1代表女性，同时移动数据以便更容易处理：NameWeight (减 135)Height (减 66)GenderAlice-2-11Bob2560Charlie1740Diana-15-61我随意选择了移位量（135和66）使数字看起来不错。通常，您会平均地移动。损失（loss）在训练我们的网络之前，我们首先需要一种方法来量化其运行状况的“良好”程度，以便它可以尝试做到“更好”。这个指标就是损失。我们使用均方误差（mean squared error，MSE）来衡量损失：$$\mathrm { MSE } = \frac { 1 } { n } \sum _ { i = 1 } ^ { n } \left( y _ { t r u e } - y _ { p r e d } \right) ^ { 2 }$$让我们分解这个公式：n是样本数，即4（Alice, Bob, Charlie, Diana）。y表示要预测的变量，即性别。$y_{true}$是变量的真实值（“正确答案”）。例如，Alice的$y_{true}$是1。$y_ {pred}$是变量的预测值，即神经网络的输出值。$(y _ { t r u e } - y _ { p r e d })^2$就是我们常说的方差，我们的损失函数取了方差的平均值，因此称其为均方误差，均方误差越小，代表模型的预测效果越好。训练网络=尽量减少其损失。损失计算示例假设我们的网络总是输出 0，换句话说，我们确信所有人都是Male。我们的损失是什么？name$y_{true}$$y_{true}$$(y _ { t r u e } - y _ { p r e d })^2 $Alice101Bob000Charlie000Diana101$$\mathrm { MSE } = \frac { 1 } { 4 } ( 1 + 0 + 0 + 1 ) = 0.5$$MSE的代码实现import numpy as np def mse_loss(y_true, y_pred): # y_true and y_pred are numpy arrays of the same length. return ((y_true - y_pred) ** 2).mean() y_true = np.array([1, 0, 0, 1]) y_pred = np.array([0, 0, 0, 0]) print(mse_loss(y_true, y_pred)) # 0.5如果您不明白代码，请阅读有关数组操作的NumPy快速入门。Nice!继续训练神经网络，第二部分我们现在有一个明确的目标：最小化神经网络的损失。我们知道我们可以更改网络的权重和偏置以影响其预测，但是我们如何以减少损失的方式做到这一点呢？本节使用了一些多元微分。如果您对微积分不熟，请随时跳过数学部分。为了简单起见，我们假设我们的数据集中只有Alice：NameWeight (minus 135)Height (minus 66)GenderAlice-2-11于是该模型的均方误差即Alice的方差：$$\begin{aligned} \mathrm { MSE } & = \frac { 1 } { 1 } \sum _ { i = 1 } ^ { 1 } \left( y _ { t r u e } - y _ { p r e d } \right) ^ { 2 } \\ & = \left( y _ { t r u e } - y _ { p r e d } \right) ^ { 2 } \\ & = \left( 1 - y _ { p r e d } \right) ^ { 2 } \end{aligned}$$考虑损失的另一种方法是权重和偏置的函数。让我们在网络中标记每个权重和偏置： 于是我们可以重写损失函数为一个多变量函数：$$L \left( w _ { 1 } , w _ { 2 } , w _ { 3 } , w _ { 4 } , w _ { 5 } , w _ { 6 } , b _ { 1 } , b _ { 2 } , b _ { 3 } \right)$$如果我们调整w1，那么损失L会如何变化？偏微分（partial derivative,$\frac { \partial L } { \partial w _ { 1 } }$）可以回答这个问题，我们怎么计算它呢？这是数学开始变得更复杂的地方。不要气馁!！我建议您随身携带一支笔和纸-它会帮助您理解。数学推导首先，用$\frac { \partial y _ { p r e d } } { \partial w _ { 1 } }$的形式重写偏微分(链式法则)：$$\frac { \partial L } { \partial w _ { 1 } } = \frac { \partial L } { \partial y _ { p r e d } } * \frac { \partial y _ { p r e d } } { \partial w _ { 1 } }$$我们可以计算$\frac { \partial L } { \partial y _ { p r e d } }$，因为有$L = \left( 1 - y _ { p r e d } \right) ^ { 2 }$$$\frac { \partial L } { \partial y _ { p r e d } } = \frac { \partial \left( 1 - y _ { p r e d } \right) ^ { 2 } } { \partial y _ { p r e d } } = - 2 \left( 1 - y _ { p r e d } \right)$$接下来考虑$\frac { \partial y _ { p r e d } } { \partial w _ { 1 } }$，和上面一样，h1,h2,o1代表相应神经元的输出，于是有：$$y _ { p r e d } = o _ { 1 } = f \left( w _ { 5 } h _ { 1 } + w _ { 6 } h _ { 2 } + b _ { 3 } \right)$$f是激活函数，还记得吗？因为w1只影响h1（不是h2），那么有：$$\begin{array} { c } { \frac { \partial y _ { p r e d } } { \partial w _ { 1 } } = \frac { \partial y _ { p r e d } } { \partial h _ { 1 } } * \frac { \partial h _ { 1 } } { \partial w _ { 1 } } } \\ { \frac { \partial y _ { p r e d } } { \partial h _ { 1 } } = w _ { 5 } * f ^ { \prime } \left( w _ { 5 } h _ { 1 } + w _ { 6 } h _ { 2 } + b _ { 3 } \right) } \end{array}$$为$\frac { \partial h _ { 1 } } { \partial w _ { 1 } }$做同样的处理：$$\begin{aligned} h _ { 1 } & = f \left( w _ { 1 } x _ { 1 } + w _ { 2 } x _ { 2 } + b _ { 1 } \right) \\ \\ \frac { \partial h _ { 1 } } { \partial w _ { 1 } } & = { x _ { 1 } * f ^ { \prime } \left( w _ { 1 } x _ { 1 } + w _ { 2 } x _ { 2 } + b _ { 1 } \right) } \end{aligned}$$x1是体重，x2是身高，现在，让我们对f(x)求导：$$\begin{array} { c } { f ( x ) = \frac { 1 } { 1 + e ^ { - x } } } \\ { f ^ { \prime } ( x ) = \frac { e ^ { - x } } { \left( 1 + e ^ { - x } \right) ^ { 2 } } = f ( x ) * ( 1 - f ( x ) ) } \end{array}$$于是，我们将$\frac { \partial L } { \partial w _ { 1 } }$分解为一系列我们可以计算的值：$$\frac { \partial L } { \partial w _ { 1 } } = \frac { \partial L } { \partial y _ { p r e d } } * \frac { \partial y _ { p r e d } } { \partial h _ { 1 } } * \frac { \partial h _ { 1 } } { \partial w _ { 1 } }$$这种不断向后计算偏微分的方法叫做反向传播(Backpropagation)例子：计算偏微分我们仍然假设数据集中只有AliceNameWeight (minus 135)Height (minus 66)GenderAlice-2-11让我们初始化权重为1，偏置为0，如果我们通过神经网络做一个前馈，可以得到：$$\begin{aligned} h _ { 1 } & = f \left( w _ { 1 } x _ { 1 } + w _ { 2 } x _ { 2 } + b _ { 1 } \right) \\ & = f ( - 2 + - 1 + 0 ) \\ & = 0.0474 \\ \\ h _ { 2 } = f & \left( w _ { 3 } x _ { 1 } + w _ { 4 } x _ { 2 } + b _ { 2 } \right) = 0.0474 \\ \\ o _ { 1 } & = f \left( w _ { 5 } h _ { 1 } + w _ { 6 } h _ { 2 } + b _ { 3 } \right) \\ & = f ( 0.0474 + 0.0474 + 0 ) \\ & = 0.524 \end{aligned}$$网络预测结果为0.524，并不是强烈地说明了性别（0或1），让我们计算$\frac { \partial L } { \partial w _ { 1 } }$$$\begin{aligned} \frac { \partial L } { \partial w _ { 1 } } = \frac { \partial L } { \partial y _ { p r e d } } * \frac { \partial y _ { p r e d } } { \partial h _ { 1 } } * \frac { \partial h _ { 1 } } { \partial w _ { 1 } } \\ \\ \frac { \partial L } { \partial y _ { p r e d } } = - 2 \left( 1 - y _ { p r e d } \right) = - 2 ( 1 - 0.524 ) = - 0.952 \end{aligned}$$$$\begin{aligned} \frac { \partial y _ { p r e d } } { \partial h _ { 1 } } = w _ { 5 } * f ^ { \prime } \left( w _ { 5 } h _ { 1 } + w _ { 6 } h _ { 2 } + b _ { 3 } \right) \\ = 1 * f ^ { \prime } ( 0.0474 + 0.0474 + 0 ) \\ = f ( 0.0948 ) * ( 1 - f ( 0.0948 ) ) \\ = 0.249 \\ \frac { \partial h _ { 1 } } { \partial w _ { 1 } } = - 2 * f ^ { \prime } ( - 2 + - 1 + 0 ) \\ = - 2 * f ( - 3 ) * ( 1 - f ( - 3 ) ) \\ = - 0.0904 \\ \frac { \partial L } { \partial w _ { 1 } } = - 0.952 * 0.249 * - 0.0904 = 0.0214 \end{aligned}$$记住我们先前得到了$f ^ { \prime } ( x ) = f ( x ) * ( 1 - f ( x ) )$这个结果告诉我们，如果w1增加，那么损失L也会增加一点点。训练：随机梯度下降（Stochastic Gradient Descent）我们现在拥有训练神经网络所需的所有工具！接下来我们将使用一种称为随机梯度下降（SGD）的优化算法，该算法可以告诉我们如何更改权重和偏置以最大程度地减少损失。基本上就是这个更新方程：$$w _ { 1 } \leftarrow w _ { 1 } - \eta \frac { \partial L } { \partial w _ { 1 } }$$η是一个常数，称为学习率( learning rate)，它控制我们训练的速度。我们要做的就是从w1中减去$\eta \frac { \partial L } { \partial w _ { 1 } }$如果$\frac { \partial L } { \partial w _ { 1 } }$是正的，那么w1会降低，L也会降低如果$\frac { \partial L } { \partial w _ { 1 } }$是负的，那么w1会增加，L也会增加如果我们对网络中的每个权重和偏置都这样做，那么损失将逐渐减少，我们的网络将得到改善。我们的训练过程将如下所示：从我们的数据集中选择一个样本。这就是它导致随机梯度下降的原因-我们一次只能处理一个样本。计算所有关于权重或偏置的损失偏导数（例如$\frac { \partial L } { \partial w _ { 1 } }$,$\frac { \partial L } { \partial w _ { 2 } }$等）。使用更新方程更新每个权重和偏置。返回步骤1。接下来让我们看看它的实际应用！代码：完整的神经网络NameWeight (minus 135)Height (minus 66)GenderAlice-2-11Bob2560Charlie1740Diana-15-61 import numpy as np def sigmoid(x): # Sigmoid activation function: f(x) = 1 / (1 + e^(-x)) return 1 / (1 + np.exp(-x)) def deriv_sigmoid(x): # Derivative of sigmoid: f'(x) = f(x) * (1 - f(x)) fx = sigmoid(x) return fx * (1 - fx) def mse_loss(y_true, y_pred): # y_true and y_pred are numpy arrays of the same length. return ((y_true - y_pred) ** 2).mean() class OurNeuralNetwork: ''' A neural network with: - 2 inputs - a hidden layer with 2 neurons (h1, h2) - an output layer with 1 neuron (o1) *** DISCLAIMER ***: The code below is intended to be simple and educational, NOT optimal. Real neural net code looks nothing like this. DO NOT use this code. Instead, read/run it to understand how this specific network works. ''' def __init__(self): # Weights self.w1 = np.random.normal() self.w2 = np.random.normal() self.w3 = np.random.normal() self.w4 = np.random.normal() self.w5 = np.random.normal() self.w6 = np.random.normal() # Biases self.b1 = np.random.normal() self.b2 = np.random.normal() self.b3 = np.random.normal() def feedforward(self, x): # x is a numpy array with 2 elements. h1 = sigmoid(self.w1 * x[0] + self.w2 * x[1] + self.b1) h2 = sigmoid(self.w3 * x[0] + self.w4 * x[1] + self.b2) o1 = sigmoid(self.w5 * h1 + self.w6 * h2 + self.b3) return o1 def train(self, data, all_y_trues): ''' - data is a (n x 2) numpy array, n = # of samples in the dataset. - all_y_trues is a numpy array with n elements. Elements in all_y_trues correspond to those in data. ''' learn_rate = 0.1 epochs = 1000 # number of times to loop through the entire dataset for epoch in range(epochs): for x, y_true in zip(data, all_y_trues): # --- Do a feedforward (we'll need these values later) sum_h1 = self.w1 * x[0] + self.w2 * x[1] + self.b1 h1 = sigmoid(sum_h1) sum_h2 = self.w3 * x[0] + self.w4 * x[1] + self.b2 h2 = sigmoid(sum_h2) sum_o1 = self.w5 * h1 + self.w6 * h2 + self.b3 o1 = sigmoid(sum_o1) y_pred = o1 # --- Calculate partial derivatives. # --- Naming: d_L_d_w1 represents "partial L / partial w1" d_L_d_ypred = -2 * (y_true - y_pred) # Neuron o1 d_ypred_d_w5 = h1 * deriv_sigmoid(sum_o1) d_ypred_d_w6 = h2 * deriv_sigmoid(sum_o1) d_ypred_d_b3 = deriv_sigmoid(sum_o1) d_ypred_d_h1 = self.w5 * deriv_sigmoid(sum_o1) d_ypred_d_h2 = self.w6 * deriv_sigmoid(sum_o1) # Neuron h1 d_h1_d_w1 = x[0] * deriv_sigmoid(sum_h1) d_h1_d_w2 = x[1] * deriv_sigmoid(sum_h1) d_h1_d_b1 = deriv_sigmoid(sum_h1) # Neuron h2 d_h2_d_w3 = x[0] * deriv_sigmoid(sum_h2) d_h2_d_w4 = x[1] * deriv_sigmoid(sum_h2) d_h2_d_b2 = deriv_sigmoid(sum_h2) # --- Update weights and biases # Neuron h1 self.w1 -= learn_rate * d_L_d_ypred * d_ypred_d_h1 * d_h1_d_w1 self.w2 -= learn_rate * d_L_d_ypred * d_ypred_d_h1 * d_h1_d_w2 self.b1 -= learn_rate * d_L_d_ypred * d_ypred_d_h1 * d_h1_d_b1 # Neuron h2 self.w3 -= learn_rate * d_L_d_ypred * d_ypred_d_h2 * d_h2_d_w3 self.w4 -= learn_rate * d_L_d_ypred * d_ypred_d_h2 * d_h2_d_w4 self.b2 -= learn_rate * d_L_d_ypred * d_ypred_d_h2 * d_h2_d_b2 # Neuron o1 self.w5 -= learn_rate * d_L_d_ypred * d_ypred_d_w5 self.w6 -= learn_rate * d_L_d_ypred * d_ypred_d_w6 self.b3 -= learn_rate * d_L_d_ypred * d_ypred_d_b3 # --- Calculate total loss at the end of each epoch if epoch % 10 == 0: y_preds = np.apply_along_axis(self.feedforward, 1, data) loss = mse_loss(all_y_trues, y_preds) print("Epoch %d loss: %.3f" % (epoch, loss)) # Define dataset data = np.array([ [-2, -1], # Alice [25, 6], # Bob [17, 4], # Charlie [-15, -6], # Diana ]) all_y_trues = np.array([ 1, # Alice 0, # Bob 0, # Charlie 1, # Diana ]) # Train our neural network! network = OurNeuralNetwork() network.train(data, all_y_trues)该代码同时可以在github上找到随着网络的学习，我们的损失稳步减少： 现在，我们可以使用神经网络来预测性别：# Make some predictions emily = np.array([-7, -3]) # 128 pounds, 63 inches frank = np.array([20, 2]) # 155 pounds, 68 inches print("Emily: %.3f" % network.feedforward(emily)) # 0.951 - F print("Frank: %.3f" % network.feedforward(frank)) # 0.039 - M总结你做到了！快速回顾一下我们所做的事情：引入了神经元（neurons），这是神经网络的基础。在我们的神经元中使用了S型激活函数（sigmoid activation function）。连接神经元使其成为神经网络（neural networks）。创建了一个数据集，其中“体重”和“身高”作为输入（或特征，features），而“性别”作为输出（或标签， label）。了解损失函数（loss functions）和均方误差（MSE）。意识到神经网络训练只是将其损失降到最低。使用反向传播（Backpropagation）来计算偏导数。使用随机梯度下降（SGD）训练我们的网络。
利用python进行数据收集、整理、分析和绘图的全过程
这是github上的一个项目，作为数据分析基础入门是非常好的，于是我把它译为中文，希望对大家有所帮助。原文地址代码均已在本地测试通过。要求在此存储库中，我将记录收集数据，处理数据并进行可视化的完整过程。该项目中使用的数据集是国家数据-社会保障卡应用中心的婴儿名字，其中包括1880年至2018年的记录。需要安装python3与以下库：Requests - 下载数据集.pandas - 进行数据分析.NumPy - 矩阵运算.Matplotlib - 绘图.seaborn - 美化图.获取数据import requests url = "https://www.ssa.gov/oact/babynames/names.zip" with requests.get(url) as response: with open("names.zip", "wb") as temp_file: temp_file.write(response.content)提示：如果因为网速等问题下载失败，可手动复制链接到浏览器中下载。这就将数据的zip包下载到我们电脑中了，它包括了各年份数据的txt文件与一个说明pdf文档，我们需要做的是读取txt文件，进行处理并保存到csv中。这个新的csv文件将成为我们的数据集，并将包含4个字段（年份，名称，性别和数量）import csv from zipfile import ZipFile # 这两个都是内置模块，无需安装 data_list = "year", "name", "gender", "count" # 此列表将保存我们的所有数据。我们使用标题行对其进行初始化。 with ZipFile("names.zip") as temp_zip: # 读取zip对象 for file_name in temp_zip.namelist(): # 读取文件列表 if ".txt" in file_name: # 只读取txt文档 with temp_zip.open(file_name) as temp_file: # 从压缩包中读取这个txt for line in temp_file.read().decode("utf-8").splitlines(): # 文件以二进制形式打开，我们使用utf-8对其进行解码，以便可以将其作为字符串进行操作。 # 准备好我们所需的数据字段并将其添加到列表中. line_chunks = line.split(",") year = file_name[3:7] name = line_chunks[0] gender = line_chunks[1] count = line_chunks[2] data_list.append([year, name, gender, count]) # 保存成csv csv.writer(open("data.csv", "w", newline="", encoding="utf-8")).writerows(data_list)首先我们声明一个data_list，它是一个二维数组。目的是为了稍后我们可以用csv.writer.writerows()方法保存到csv中。（我更喜欢用writerows()而不是writerow()，因为它更快）接下来的步骤都有注释，就不再赘述了。数据探索（Data Exploration）基础信息首先导入相关库import matplotlib.pyplot as plt import numpy as np import pandas as pd import seaborn as sns接着我们使用pandas.read_csv()方法读取csvdf = pd.read_csv("data.csv") # 由于我们的数据集非常简单，因此我们不需要设置任何特殊标志或额外参数，默认值就足够了。让我们从经典的df.head()和df.tail()方法开始，他们分别能查看数据集前几行和后几行。df.head() # 前五行 >>> year name gender count 0 1880 Mary F 7065 1 1880 Anna F 2604 2 1880 Emma F 2003 3 1880 Elizabeth F 1939 4 1880 Minnie F 1746 df.tail() # 后五行 >>> year name gender count 1957041 2018 Zylas M 5 1957042 2018 Zyran M 5 1957043 2018 Zyrie M 5 1957044 2018 Zyron M 5 1957045 2018 Zzyzx M 5这些样本告诉我们以下信息：有4列（年份，名称，性别和计数）。有1,957,046行。行按年份排序。女性记录显示在男性记录之前。2018年，至少有5个父母将其儿子命名为``Zzyzx''。唯一名称df["name"].nunique() # 拥有独一无二名字的人有多少 df[df["gender"] == "M"]["name"].nunique() # 女性拥有独一无二名字的人有多少 df[df["gender"] == "F"]["name"].nunique() # 男性拥有独一无二名字的人有多少 # 性别中立的人有多少拥有独一无二的名字 both_df = df.pivot_table(index="name", columns="gender", values="count", aggfunc=np.sum).dropna() both_df.index.nunique()可以看出，数据集中有98,400个人拥有唯一名称。其中，男性41,475名，女性67,698名，性别中立10,773名。十大常用名字为了获得前10个最常用的男性和女性名字，我们将首先用dataframe按性别过滤。有了特定性别dataframe后，我们将只选择2个字段，即姓名和人数。然后在name字段上使用groupby()方法，并使用sum()汇总结果。最后，我们将对count字段上的值进行降序排序，并使用head(10)方法获得前10个结果。df = df[df["gender"] == "M"] df = df"name", "count" df = df.groupby("name") df = df.sum() df = df.sort_values("count", ascending=False) df.head(10) >>> name count James 5164280 John 5124817 Robert 4820129 Michael 4362731 William 4117369 David 3621322 Joseph 2613304 Richard 2565301 Charles 2392779 Thomas 2311849上面的代码可以写到一起，即：df[df["gender"] == "M"]"name", "count".groupby("name").sum().sort_values("count", ascending=False).head(10)当然，把"gender"改成"F"就可以找女性十大常用名了。前二十大性别中立者常用名这有点挑战，首先我们需要旋转dataframe，以便名称成为索引，性别成为列，所有计数的总和（每个名称，每个性别）将成为我们的值。我们将分步骤进行。首先，我们旋转表并删除值为0的行。即名称不在男性或女性中的行。df = df.pivot_table(index="name", columns="gender", values="count", aggfunc=np.sum).dropna()会输出类似的数据：gender F M name Aaden 5.0 4828.0 Aadi 16.0 851.0 Aadyn 16.0 516.0 Aalijah 149.0 212.0 Aaliyah 87442.0 96.0 ... ...有了这种形状的数据，我们现在知道每个姓名每个性别有多少条记录。现在，我们仅考虑那些至少具有50,000个性别记录的名称。df = df[(df["M"] >= 50000) & (df["F"] >= 50000)] df.head(20)注意：原本我只使用前10个名字，但由于只有20个符合我们标准的名字，我将其推到了20个。姓名记录最高和最低的年份现在，我们将知道哪个年份的记录之和最高和最低。both_df = df.groupby("year").sum() male_df = df[df["gender"] == "M"].groupby("year").sum() female_df = df[df["gender"] == "F"].groupby("year").sum() # 组合最小值（年份和计数） both_df.min()["count"] both_df.idxmin()["count"] # 男性最小值（年份和计数） male_df.min()["count"] male_df.idxmin()["count"] # Female Min (count and year) female_df.min()["count"] female_df.idxmin()["count"] # Combined Max (count and year) both_df.max()["count"] both_df.idxmax()["count"] # Male Max (count and year) male_df.max()["count"] male_df.idxmax()["count"] # Female Max (count and year) female_df.max()["count"] female_df.idxmax()["count"]绘图在此项目中，我们仅使用线绘图，这对于显示值随时间的变化非常有帮助。首先要做的是设置一些自定义颜色，这些颜色将全局应用于每个图。# 这些参数生成带有淡紫色的图。 sns.set(style="ticks", rc={ "figure.figsize": [12, 7], "text.color": "white", "axes.labelcolor": "white", "axes.edgecolor": "white", "xtick.color": "white", "ytick.color": "white", "axes.facecolor": "#443941", "figure.facecolor": "#443941"} )声明样式后，我们就可以绘制数据了。为了效率，接下来我会使用单线，但是不用担心，我会解释每条线的作用。按年份计数我们第一个线绘图将展示历年姓名记录数据的变化。首先，为男性、女性、组合创建新的dataframesboth_df = df.groupby("year").sum() male_df = df[df["gender"] == "M"].groupby("year").sum() female_df = df[df["gender"] == "F"].groupby("year").sum()我们直接绘制dataframes。索引为x轴，总数为y轴。plt.plot(both_df, label="Both", color="yellow") plt.plot(male_df, label="Male", color="lightblue") plt.plot(female_df, label="Female", color="pink")设置y轴的间距为500,000，首先我们格式化标签的数据，然后使用实际数据作为间隔。yticks_labels = ["{:,}".format(i) for i in range(0, 4500000+1, 500000)] plt.yticks(np.arange(0, 4500000+1, 500000), yticks_labels)添加一些其他设置项plt.legend() plt.grid(False) plt.xlabel("Year") plt.ylabel("Records Count") plt.title("Records per Year") plt.show() 最受欢迎名字的增长情况首先，我们合并男性和女性的值，然后旋转表，让名称是索引，年份是列。我们还用零填充缺失值。pivoted_df = df.pivot_table( index="name", columns="year", values="count", aggfunc=np.sum).fillna(0)然后按年份计算每个名字的百分比。percentage_df = pivoted_df / pivoted_df.sum() * 100增加新的一列作为百分比之和percentage_df["total"] = percentage_df.sum(axis=1)我们对dataframe进行排序以检查哪些是最高值并将进行切片。之后，我们删除total列，因为将不再使用它。sorted_df = percentage_df.sort_values( by="total", ascending=False).drop("total", axis=1)[0:10]翻转轴，以便更轻易地绘图transposed_df = sorted_df.transpose()我们通过使用列名作为标签和Y轴来单独地绘制每个名字。for name in transposed_df.columns.tolist(): plt.plot(transposed_df.index, transposed_df[name], label=name)设置y轴间距为0.5%yticks_labels = ["{}%".format(i) for i in np.arange(0, 5.5, 0.5)] plt.yticks(np.arange(0, 5.5, 0.5), yticks_labels)增加最后的自定义项plt.legend() plt.grid(False) plt.xlabel("Year") plt.ylabel("Percentage by Year") plt.title("Top 10 Names Growth") plt.show() 十大热门名字我们将获得过去10年（2008-2018）的最热门的十个名字变化趋势和上面的思路类似，直接上代码# 首先要做的是删除所有早于2008年的记录。 filtered_df = df[df["year"] >= 2008] # 然后，我们合并男性女性值，并对表进行数据透视，因此名称是我们的索引，年份是我们的列。我们还用零填充缺失值。 pivoted_df = filtered_df.pivot_table( index="name", columns="year", values="count", aggfunc=np.sum).fillna(0) # 然后我们按年份计算每个名称的百分比。 percentage_df = pivoted_df / pivoted_df.sum() * 100 # 我们添加了一个新列来表示百分比之和。 percentage_df["total"] = percentage_df.sum(axis=1) # 我们对dataframe进行排序以检查哪些是最高值并将进行切片。之后，我们删除total列，因为将不再使用它。 sorted_df = percentage_df.sort_values( by="total", ascending=False).drop("total", axis=1)[0:10] # 翻转轴，以便更轻松地绘制数据框。 transposed_df = sorted_df.transpose() # 我们通过使用列名作为标签和Y轴来单独地绘制每个名字。 for name in transposed_df.columns.tolist(): plt.plot(transposed_df.index, transposed_df[name], label=name) # Y轴0.5%的间距 yticks_labels = ["{:.2f}%".format(i) for i in np.arange(0.3, 0.7, 0.05)] plt.yticks(np.arange(0.3, 0.7, 0.05), yticks_labels) # 从2008年到2018年，我们将x轴的间距设为1 xticks_labels = ["{}".format(i) for i in range(2008, 2618+1, 1)] plt.xticks(np.arange(2008, 2018+1, 1), xticks_labels) # 最后设置 plt.legend() plt.grid(False) plt.xlabel("Year") plt.ylabel("Percentage by Year") plt.title("Top 10 Trending Names") plt.show()