神经网络入门

前言

本文主要介绍最基础的神经网络,包括其结构,学习方法, C++ \texttt{C++} C++ 的实现代码。 Python \texttt{Python} Python 的代码可以搜索互联网得到。

前排提示:本人涉及一丁点数学知识。

神经网络的结构

神经网络包括多个层次(Layer)。一般来说神经网络包括三个部分:输入层隐藏层输出层 。输入层和输出层都只有一个,而隐藏层可以有多个。顺序为 输入层,隐藏层,输出层。

每一个层次会有若干的神经元。我们称第 L l L_l Ll 层为输出层,也就是最后一层。对于第 L L L 层(从 0 0 0 开始计数),我们用 n L n_L nL 表示其神经元的数量。

对于第 L L L 层的第 i i i 个神经元(从 0 0 0 开始计数),我们用 a i L a^L_i aiL 表示其输出到下一层神经元或者作为结果的值。对于生物体的神经元, a i L a_i^L aiL 的取值只有 0 , 1 0,1 0,1 ,但是对于本文而言,计算机的神经元有 a i L ∈ [ 0 , 1 ] a_i^L\in[0,1] aiL[0,1] 。同时,每个神经元还有一个偏移值 b i L b^L_i biL

除输入层外,每一个神经元都与上一层的所有神经元都有一个连接,对于第 L ( 1 < L ) L(1<L) L(1<L) 的第 i i i 个神经元,它与 L − 1 L-1 L1 层的所有神经元都有连接,其中它与第 j j j 个神经元的连接有权重 w i , j L w^L_{i,j} wi,jL

下图展示了一个简单的神经网络的构成:

图中有 n 0 = 3 , n 1 = n 2 = 2 n_0=3,n_1=n_2=2 n0=3,n1=n2=2 ,且第 0 0 0 层为输入层,第 2 2 2 层为输出层。

PS \texttt{PS} PS: 此图没有展示神经元的偏移值。

在这里插入图片描述

输出值的计算

对于输入层而言,其输出值 a i 0 ( i ∈ [ 0 , n 0 ) ∩ Z ) a^0_i (i\in[0,n_0)\cap \mathbb{Z}) ai0(i[0,n0)Z) ,其输出值与神经网络的输入相同。

对于隐藏层和输出层,其计算方式如下:
a i L = f ( z i L ) z i L = b i L + ∑ j = 0 n L − 1 w i , j L a j L − 1 \begin{aligned} a^L_i&=f\left(z^L_i\right)\\ z^L_i&=b^L_i + \sum_{j=0}^{n_{L-1}} w^L_{i,j}a^{L-1}_j \end{aligned} aiLziL=f(ziL)=biL+j=0nL1wi,jLajL1
其中, f ( x ) f(x) f(x) 函数一般为 S i g m o i d \mathrm{Sigmoid} Sigmoid 函数,常见于生物种群数量的自然增长。其定义和导数如下:
f ( x ) = 1 1 + e − x f ′ ( x ) = f ( x ) ( 1 − f ( x ) ) \begin{aligned} f(x)&=\frac{1}{1+e^{-x}}\\ f'(x)&=f(x)(1-f(x)) \end{aligned} f(x)f(x)=1+ex1=f(x)(1f(x))
这个函数可以让我们的输出值取值范围固定在 [ 0 , 1 ] [0,1] [0,1] 之间。

实际上,神经网络使用的 f ( x ) f(x) f(x) 不一定需要让输出值固定在 [ 0 , 1 ] [0,1] [0,1] 之间,比如卷积神经网络(CNN)中有一个 f ( x ) f(x) f(x) R e L U \mathrm{ReLU} ReLU 函数,其定义为:
f ( x ) = { x x > 0 0 o t h e r w i s e f(x)=\begin{cases} x & x>0\\ 0 & \mathrm{otherwise} \end{cases} f(x)={x0x>0otherwise

请留意 z i L z^L_i ziL 这个值,我们暂且将其称为成为中间值

神经网络的学习方法

整个神经网络的计算过程,可以认为是一个庞大的函数 F F F ,其自变量的个数是输入层神经元的个数,而因变量的个数是输出层神经元的个数。而我们需要的让这个函数去拟合一个标准函数 G G G

而神经网络的学习过程,实际上是一个用提供的若干个输入输出(我们称这些输入输出为训练集)不断调整权重 w w w 以及偏移值 b b b ,使得 F F F 拟合 G G G 的过程。

过拟合

实际上在神经网络的学习过程中,因为参数过多(或者其他原因),容易出现过拟合(overfitting)的现象。表现为在对于训练集中的数据,神经网络可以给出优秀的结果,但是对于训练集以外的数据,则出现很多错误。

一个形象的理解方法就是:当我们用给定的一些三次函数 G G G 上的点,用一个 6 6 6 次函数 F F F 去拟合这个 3 3 3 次函数 G G G 的时候,对于给定的点, F F F 都和 G G G 的输出一致,但是本应该是 0 0 0 4 , 5 , 6 4,5,6 4,5,6 次项不全是 0 0 0 ,导致 F F F 会出现一些弯曲,这时候就出现了过拟合。下图是一个过拟合的示意图:

在这里插入图片描述

其中绿色的点是训练集, G ( x ) G(x) G(x) 是我们需要的函数, F ( x ) F(x) F(x) 为神经网络拟合出的函数。

一般来说,我们会使用另一套输入输出(我们称之为测试集)来评判神经网络的学习效果。

在当前状态下,我们并不考虑过拟合问题。

反向传播方法(Backprop,简称BP)

反向传播方法是神经网络学习的核心算法。

对于训练集中的一个输入输出 k k k ,我们用 O i O_i Oi 表示理想状态下,神经网络的输出层的第 i i i 个神经元应该有的输出值。 这时候,我们可以计算出神经网络当前的函数 F F F G G G 的误差,这里我们用类似方差的表达式表示其误差:
C k = ∑ i = 0 N L l ( a i L l − O i ) 2 C_k=\sum_{i=0}^{N_{L_l}} (a^{L_l}_i-O_i)^2 Ck=i=0NLl(aiLlOi)2

实际上有其他表示误差的方式,本人不是很了解。

显然的,我们需要让 C k C_k Ck 越小越好,这时候,我们需要知道 a i L l a_i^{L_l} aiLl 应该如何变化才能使得 C k C_k Ck 变小,可以想到运用导数计算出 a a a 应该有的变化,也就是:
∂ C k ∂ a i L l = 2 ( a i L l − O i ) \frac{\partial C_k}{\partial a^{L_l}_i} =2\left(a^{L_l}_i - O_i\right) aiLlCk=2(aiLlOi)
想让 C k C_k Ck 变小,就需要让 a i L l a_i^{L_l} aiLl 向着 − ∂ C k a i L l -\frac{\partial C_k}{a^{L_l}_i} aiLlCk 方向减小,至于如何做到这一点,我们可以调整 b i L l , w i , ∗ L l , a ∗ L l − 1 b^{L_l}_i,w^{L_l}_{i,*}, a^{L_{l-1}}_* biLl,wi,Ll,aLl1。而调整这些参数,也可以使用求导的方式 ( 0 ≤ j < n l − 1 0\leq j<n_{l-1} 0j<nl1):
∂ C k ∂ b i L l = ∂ z i L l ∂ b i L l ∂ a i L l ∂ z i L l ∂ C k ∂ a i L l = 1 ⋅ f ′ ( z i L l ) ∂ C k ∂ a i L l ∂ C k ∂ w i , j L l = ∂ z i L l ∂ w i , j L l ∂ a i L l ∂ z i L l ∂ C k ∂ a i L l = a j L l − 1 f ′ ( z i L l ) ∂ C k ∂ a i L l ∂ C k ∂ a j L l − 1 = ∑ i = 0 n L l ∂ z i L l ∂ a j L l − 1 ∂ a i L l ∂ z i L l ∂ C k ∂ a i L l = ∑ i = 0 n L l − 1 w i , j L l f ′ ( z i L l ) ∂ C k ∂ a i L l \begin{aligned} \frac{\partial C_k}{\partial b^{L_l}_i} &= \frac{\partial z^{L_l}_i}{\partial b^{L_l}_i} \frac{\partial a^{L_l}_i}{\partial z^{L_l}_i} \frac{\partial C_k}{\partial a^{L_l}_i} = 1\cdot f'\left(z^{L_l}_i\right)\frac{\partial C_k}{\partial a^{L_l}_i}\\ \frac{\partial C_k}{\partial w^{L_l}_{i,j}} &= \frac{\partial z^{L_l}_i}{\partial w^{L_l}_{i,j}} \frac{\partial a^{L_l}_i}{\partial z^{L_l}_i} \frac{\partial C_k}{\partial a^{L_l}_i} = a^{L_{l}-1}_j f'\left(z^{L_l}_i\right)\frac{\partial C_k}{\partial a^{L_l}_i}\\ \frac{\partial C_k}{\partial a^{L_{l}-1}_j} &= \sum_{i=0}^{n_{L_l}}\frac{\partial z^{L_l}_i}{\partial a^{L_{l}-1}_j} \frac{\partial a^{L_l}_i}{\partial z^{L_l}_i} \frac{\partial C_k}{\partial a^{L_l}_i} = \sum_{i=0}^{n_{L_l}-1}w^{L_l}_{i,j}f'\left(z^{L_l}_i\right)\frac{\partial C_k}{\partial a^{L_l}_i} \end{aligned} biLlCkwi,jLlCkajLl1Ck=biLlziLlziLlaiLlaiLlCk=1f(ziLl)aiLlCk=wi,jLlziLlziLlaiLlaiLlCk=ajLl1f(ziLl)aiLlCk=i=0nLlajLl1ziLlziLlaiLlaiLlCk=i=0nLl1wi,jLlf(ziLl)aiLlCk
要调整这些参数,只需要向着导数的反方向增减这些变量即可,至于 a j L l − 1 a^{L_l-1}_j ajLl1 的调整,需要调整 b j L l − 1 b^{L_l-1}_j bjLl1 以及 w j , ∗ L l − 1 w^{L_l-1}_{j,*} wj,Ll1 ,其调整方式也是求导,且结构和上述的式子大致相同:
∀ L ∈ [ 1 , L l − 1 ] ∩ Z ∂ C k ∂ b i L = ∂ z i L ∂ b i L ∂ a i L ∂ z i L ∂ C k ∂ a i L = 1 ⋅ f ′ ( z i L ) ∂ C k ∂ a i L ∂ C k ∂ w i , j L = ∂ z i L ∂ w i , j L ∂ a i L ∂ z i L ∂ C k ∂ a i L = a j L − 1 f ′ ( z i L ) ∂ C k ∂ a i L ∂ C k ∂ a j L = ∑ i = 0 n L + 1 − 1 ∂ z i L + 1 ∂ a j L ∂ a i L + 1 ∂ z i L + 1 ∂ C k ∂ a i L + 1 = ∑ i = 0 n L + 1 − 1 w i , j L + 1 f ′ ( z i L + 1 ) ∂ C k ∂ a i L + 1 \begin{aligned} &\forall L\in [1,L_l-1] \cap \mathbb{Z}\\ &\frac{\partial C_k}{\partial b^{L}_i} = \frac{\partial z^{L}_i}{\partial b^{L}_i} \frac{\partial a^{L}_i}{\partial z^{L}_i} \frac{\partial C_k}{\partial a^{L}_i} = 1\cdot f'\left(z^{L}_i\right)\frac{\partial C_k}{\partial a^{L}_i}\\ &\frac{\partial C_k}{\partial w^{L}_{i,j}} = \frac{\partial z^{L}_i}{\partial w^{L}_{i,j}} \frac{\partial a^{L}_i}{\partial z^{L}_i} \frac{\partial C_k}{\partial a^{L}_i} = a^{L-1}_j f'\left(z^{L}_i\right)\frac{\partial C_k}{\partial a^{L}_i}\\ &\frac{\partial C_k}{\partial a^{L}_j} = \sum_{i=0}^{n_{L+1}-1}\frac{\partial z^{L+1}_i}{\partial a^{L}_j} \frac{\partial a^{L+1}_i}{\partial z^{L+1}_i} \frac{\partial C_k}{\partial a^{L+1}_i} = \sum_{i=0}^{n_{L+1}-1}w^{L+1}_{i,j}f'\left(z^{L+1}_i\right)\frac{\partial C_k}{\partial a^{L+1}_i} \end{aligned} L[1,Ll1]ZbiLCk=biLziLziLaiLaiLCk=1f(ziL)aiLCkwi,jLCk=wi,jLziLziLaiLaiLCk=ajL1f(ziL)aiLCkajLCk=i=0nL+11ajLziL+1ziL+1aiL+1aiL+1Ck=i=0nL+11wi,jL+1f(ziL+1)aiL+1Ck
至此,我们得到了为了拟合数据 k k k 而需要调整的数据的内容,以及调整之前的网络的信息,我们用两个数组(或者认为是矩阵,向量)将其整合一下:
∇ C k = [ ∂ C k ∂ b 0 0 ∂ C k ∂ b 1 0 ⋮ ∂ C k ∂ b n L l − 1 L l ∂ C k ∂ w 0 , 0 1 ∂ C k ∂ w 0 , 1 1 ⋮ ∂ C k ∂ w n L l − 1 , n L l − 1 − 1 L l ] , D = [ b 0 0 b 1 0 ⋮ b n L l − 1 L l w 0 , 0 1 w 0 , 1 1 ⋮ w n L l − 1 , n L l − 1 − 1 L l ] \nabla C_k=\begin{bmatrix} \frac{\partial C_k}{\partial b^{0}_0}\\ \frac{\partial C_k}{\partial b^{0}_1}\\ \vdots\\ \frac{\partial C_k}{\partial b^{L_l}_{n_{L_l}-1}}\\ \frac{\partial C_k}{\partial w^{1}_{0,0}}\\ \frac{\partial C_k}{\partial w^{1}_{0,1}}\\ \vdots\\ \frac{\partial C_k}{\partial w^{L_l}_{n_{L_l}-1,n_{L_l-1}-1}} \end{bmatrix} , D=\begin{bmatrix} b^{0}_0\\ b^{0}_1\\ \vdots\\ b^{L_l}_{n_{L_l}-1}\\ w^{1}_{0,0}\\ w^{1}_{0,1}\\ \vdots\\ w^{L_l}_{n_{L_l}-1,n_{L_l-1}-1} \end{bmatrix} Ck= b00Ckb10CkbnLl1LlCkw0,01Ckw0,11CkwnLl1,nLl11LlCk ,D= b00b10bnLl1Llw0,01w0,11wnLl1,nLl11Ll
当我们调整网络的时候,可以使用一个参数 η \eta η 调整这个数据对学习的影响,而调整方式,可以用 D ′ = D − η ⋅ ∇ C k D'=D-\eta \cdot \nabla C_k D=DηCk 表示对这个数据的学习。可想而知 η \eta η 越大,这个数据对网络的影响越强。而 η \eta η 的调节,是一个需要讨论的问题,但是本人不太懂,所以本文中的 η = 1 \eta =1 η=1

Mini-Batch

如果对于每个数据,都进行一次调整 ,那么效率会变得很低,为此,我们可以使用一个小技巧,也就是 Mini-Batch \texttt{Mini-Batch} Mini-Batch 。具体操作方式如下:

首先设定一个大小 S S S ,本文中,我们设其为 10 10 10 。接着,将数据集打乱,然后每 S S S 个数据组成一个 B a t c h \mathrm{Batch} Batch 。对于每一个 B a t c h \mathrm{Batch} Batch ,设其中的数据编号为 1 , 2 , 3 , … , S 1,2,3,\dots, S 1,2,3,,S ,对于每一个数据 i i i ,都用 BP \texttt{BP} BP 计算出一个 ∇ C i \nabla C_i Ci ,接着将 B a t c h \mathrm{Batch} Batch 中的调整信息整合,这里我们用平均值进行整合: ∇ C = 1 S ∑ i = 1 S ∇ C i \nabla C=\frac{1}{S}\sum_{i=1}^S \nabla C_i C=S1i=1SCi ,接着用这个 ∇ C \nabla C C 调整神经网络。

代码

NN . h \texttt{NN}.h NN.h

#pragma once

#include <vector>
#include <cstdarg>
#include "gloconst.h"


class Network {
private:
	double **a, **b, **da, **db, **z;
	double ***w, ***dw;
	std::vector<int> network_size;
	/// <summary>
	/// 进行逆向传播
	/// </summary>
	/// <param name="output">学习的输出</param>
	void Backprop(double *output);
public:
	Network(std::vector<int> &network_size);
	~Network();
	
	/// <summary>
	/// 计算一个数据
	/// </summary>
	/// <param name="input">输入</param>
	void Calculate(double *input);
	/// <summary>
	/// 对模型进行训练
	/// </summary>
	/// <param name="data">数据 data[i][0] 表示第i个输入,data[i][1] 表示第i个输出</param>
	/// <param name="size">数据的数量</param>
	/// <param name="batch_size">mini-batch的大小</param>
	void Train(double ***data, int size, int batch_size = 100);
	/// <summary>
	/// 得到输出值a最大的节点的id
	/// </summary>
	/// <returns></returns>
	int GetMaxOutputNode();
};

NN.cpp \texttt{NN.cpp} NN.cpp

#include "NN.h"
#include <algorithm>
#include <ctime>
#include <cassert>
#include <random>

unsigned seed = time(NULL);
std::default_random_engine gen(seed);
std::normal_distribution<double> dis(0, 1);

//生成正态分布的随机数 mu=0,sigma=1
inline double randn() {
	return dis(gen);
}
Network::Network(std::vector<int> &network_size) {
	
	this->network_size = network_size;
	a = new double *[network_size.size()];
	b = new double *[network_size.size()];
	da = new double *[network_size.size()];
	db = new double *[network_size.size()];
	z = new double *[network_size.size()];
	w = new double **[network_size.size() - 1];
	dw = new double **[network_size.size() - 1];

	for (int i = 0; i < network_size.size(); i++) { 
		int size_i = network_size[i];
		b[i] = new double[size_i], a[i] = new double[size_i], da[i] = new double[size_i], db[i] = new double[size_i], z[i] = new double[size_i];
		for (int j = 0; j < size_i; j++) b[i][j] = randn();
		if (i) {
			w[i] = new double *[size_i];
			dw[i] = new double *[size_i];
			for (int j = 0; j < size_i; j++) {
				int size2 = network_size[i - 1];
				w[i][j] = new double[size2];
				dw[i][j] = new double[size2];

				for (int k = 0; k < size2; k++)
					w[i][j][k] = randn();
			}
		}
	}
}

Network::~Network() {
	for (int i = 0; i < network_size.size(); i++) {
		int size_i = network_size[i];
		delete[] a[i], delete[] b[i], delete[] da[i], delete[] db[i], delete[] z[i];
		if (i) for (int j = 0; j < size_i; j++)
				delete[] w[i][j], delete[] dw[i][j];
	}
}

void Network::Calculate(double *input) {
	memcpy(a[0], input, sizeof(double) * network_size[0]);
	for (int i = 1; i < network_size.size(); i++) {
		int sizei = network_size[i], size_lst = network_size[i - 1];
		for (int j = 0; j < sizei; j++) {
			z[i][j] = b[i][j];
			for (int k = 0; k < size_lst; k++) z[i][j] += w[i][j][k] * a[i - 1][k];
			a[i][j] = Sigmoid(z[i][j]);
		}
	}
}

void Network::Backprop(double *output) {
	int lstid = network_size.size() - 1, sizei = network_size[network_size.size() - 1];
	for (int i = 0; i < sizei; i++) {
		da[lstid][i] = (a[lstid][i] - output[i]),
		db[lstid][i] = DSigmoid(z[lstid][i]) * da[lstid][i];
		for (int j = 0; j < network_size[lstid - 1]; j++)
			dw[lstid][i][j] = a[lstid - 1][j] * db[lstid][i];
	}
	for (int i = lstid - 1; i > 0; i--) {
		for (int j = 0; j < network_size[i]; j++) {
			da[i][j] = 0;
			for (int k = 0; k < network_size[i + 1]; k++)
				da[i][j] += w[i + 1][k][j] * db[i + 1][k];
			db[i][j] = DSigmoid(z[i][j]) * da[i][j];
			for (int k = 0; k < network_size[i - 1]; k++)
				dw[i][j][k] = a[i - 1][k] * db[i][j];
		}
	}
}

void array_add(double *dst, double *src, int sz) {
	for (int i = 0; i < sz; i++) dst[i] += src[i];
}
void array_mul(double *dst, double x, int sz) {
	for (int i = 0; i < sz; i++) dst[i] *= x;
}

void Network::Train(double ***data, int size, int batch_size) {
	batch_size = std::min(batch_size, size);
	std::random_shuffle(data, data + size);
	double **avr_db, ***avr_dw;
	avr_db = new double *[network_size.size()];
	avr_dw = new double **[network_size.size()];
	for (int i = 0; i < network_size.size(); i++) {
		avr_db[i] = new double[network_size[i]];
		avr_dw[i] = new double*[network_size[i]];
		if (i) for (int j = 0; j < network_size[i]; j++)
				avr_dw[i][j] = new double[network_size[i - 1]];
	}
	const double eta = 1;
	for (int st = 0; st < size; st += batch_size) {
		//clean the avr array
		for (int i = 0; i < network_size.size(); i++) {
			memset(avr_db[i], 0, sizeof(double) * network_size[i]);
			if (i) for (int j = 0; j < network_size[i]; j++)
					memset(avr_dw[i][j], 0, sizeof(double) * network_size[i - 1]);
		}
		for (int x = st; x < st + batch_size; x++) {
			Calculate(data[x][0]);
			Backprop(data[x][1]);
			for (int i = 0; i < network_size.size(); i++) {
				array_add(avr_db[i], db[i], network_size[i]);
				if (i) for (int j = 0; j < network_size[i]; j++)
						array_add(avr_dw[i][j], dw[i][j], network_size[i - 1]);
			}
			
		}
		for (int i = 1; i < network_size.size(); i++) {
			array_mul(avr_db[i], -eta / batch_size, network_size[i]);
			array_add(b[i], avr_db[i], network_size[i]);
			if (i) for (int j = 0; j < network_size[i]; j++) {
					array_mul(avr_dw[i][j], -eta / batch_size, network_size[i - 1]),
					array_add(w[i][j], avr_dw[i][j], network_size[i - 1]);
				}
		}
	}
	for (int i = 0; i < network_size.size(); i++) {
		delete[] avr_db[i];
		if (i) {
			for (int j = 0; j < network_size[i]; j++) delete[] avr_dw[i][j];
			delete[] avr_dw[i];
		}
	}
	delete[] avr_db, delete[] avr_dw;
}

int Network::GetMaxOutputNode() {
	int res = 0;
	double *lyr = a[network_size.size() - 1];
	int size = network_size[network_size.size() - 1];
	for (int i = 1; i < size; i++)
		res = (lyr[res] > lyr[i] ? res : i);
	return res;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/90868.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux--进程地址空间

1.线程地址空间 所谓进程地址空间&#xff08;process address space&#xff09;&#xff0c;就是从进程的视角看到的地址空间&#xff0c;是进程运行时所用到的虚拟地址的集合。 简单地说&#xff0c;进程就是内核数据结构和代码和本身的代码和数据&#xff0c;进程本身不能…

构建智慧停车场:4G DTU实现无线数据高速传输

物联网技术的快速发展使得各种设备能够实现互联互通&#xff0c;无线网络技术给我们的日常生活带来了极大的便利。其中的网络技术如无线WiFi及4G网络已经成为了物联网应用中不可或缺的组成部分。而在工业领域中对4G无线路由器的应用是非常广泛的&#xff0c;人们通过4G工业路由…

eclipse中设置按backspace键、或者delete键,一次删除代码中多个空格

选择菜单Window->Preferences&#xff1a; 在弹出窗口中&#xff0c;找到General->Text Editors&#xff0c;在右面的选项中勾选Insert spaces for tabs和Remove multiple spaces on backspace/delete&#xff0c;然后点击窗口下面的Applay and Close按钮&#xff1a; …

【Linux】进程通信 — 信号(上篇)

文章目录 &#x1f4d6; 前言1. 什么是信号1.1 认识信号&#xff1a;1.2 信号的产生&#xff1a;1.3 信号的异步&#xff1a;1.4 信号的处理&#xff1a; 2. 前后台进程3. 系统接口3.1 signal&#xff1a;3.1 - 1 不能被捕捉的信号 3.2 kill&#xff1a;3.2 - 1 killall 3.3 ra…

Vue3.0极速入门- 目录和文件说明

目录结构 以下文件均为npm create helloworld自动生成的文件目录结构 目录截图 目录说明 目录/文件说明node_modulesnpm 加载的项目依赖模块src这里是我们要开发的目录&#xff0c;基本上要做的事情都在这个目录里assets放置一些图片&#xff0c;如logo等。componentsvue组件…

巨人互动|Facebook海外户Facebook游戏全球发布实用策略

Facebook是全球最大的社交媒体平台之一&#xff0c;拥有庞大的用户基数和广阔的市场。对于游戏开发商而言&#xff0c;利用Facebook进行全球发布是一项重要的策略。下面小编将介绍一些实用的策略帮助开发商在Facebook上进行游戏全球发布。 巨人互动|Facebook海外户&Faceboo…

Maven的超级POM

对于我们创建的一个maven工程&#xff0c;即便我们自己的pom.xm文件中没有明确指定一个父工程&#xff08;父POM&#xff09;&#xff0c;其实也默认继承了超级POM&#xff0c;就好比JAVA类继承Object类一样。 maven官网关于超级POM的介绍&#xff1a; https://maven.apache.o…

3.BGP状态机和路由注入方式

BGP状态机 BGP路由的生成 不同于IGP路由协议,BGP自身并不会发现并计算产生路由,BGP将GP路由表中的路由注入到BGP路由表中,并通过Update报文传递给BGP对等体。 BGP注入路由的方式有两种: Networkimport-route与IGP协议相同,BGP支持根据已有的路由条目进行聚合,生成聚合路由…

关于css 的选择器和 css变量

css 选择器 常用的选择器 1. 后代选择器&#xff1a;也就是我们常见的空格选择器&#xff0c;选择的对象为该元素下的所有子元素 。例如&#xff0c;选择所有 元素下的 元素 div p{font-size:14px}2. 子元素选择器 ‘>’ 选择某元素下的直接子元素。例如&#xff0c;选择所…

stm32之8.中断

&#xff08;Exceptions&#xff09;异常是导致程序流更改的事件&#xff0c;发生这种情况&#xff0c;处理器将挂起当前执行的任务&#xff0c;并执行程序的一部分&#xff0c;称之为异常处理函数。在完成异常处理程序的执行之后&#xff0c;处理器将恢复正常的程序执行&#…

PyQt open3d 加载 显示点云

PyQt加载 显示点云&#xff0c;已经有三种方式&#xff0c;使用 open3d; 使用 vtk; 使用 pcl; 下面是使用 open3d: import sys import open3d as o3d import numpy as np import pyqtgraph.opengl as gl from PyQt5.QtWidgets import QApplication, QVBoxLayout, QWidget, QFi…

LeetCode 138.复制带随机指针的链表

文章目录 &#x1f4a1;题目分析&#x1f4a1;解题思路&#x1f6a9;步骤一&#xff1a;拷贝节点插入到原节点的后面&#x1f369;步骤一代码 &#x1f6a9;步骤二&#xff1a;控制拷贝节点的random进行连接&#x1f369;步骤二代码 &#x1f6a9;步骤三&#xff1a;拷贝节点解…

Linux 内核page migration设计文档

概述 page migration设计之初是在numa system的各个node之间迁移physical pages&#xff0c;意味着进程页面的虚拟地址不会变化&#xff0c;物理地址发生改变&#xff0c;migration的目的将page迁移到临近的cpu上降低内存访问延迟。 页面迁移粗略步骤 A. In kernel use of m…

4.14 tcp_tw_reuse 为什么默认是关闭的?

开启 tcp_tw_reuse 参数可以快速复用处于 TIME_WAIT 状态的 TCP 连接时&#xff0c;相当于缩短了 TIME_WAIT 状态的持续时间。 tcp_tw_reuse 是什么&#xff1f; TIME_WAIT 状态的持续时间是 60 秒&#xff0c;这意味着这 60 秒内&#xff0c;客户端一直会占用着这个端口。端…

桃子叶片病害识别(Python代码,pyTorch框架,深度卷积网络模型,很容易替换为其它模型,带有GUI识别界面)

1.分为三类 健康的桃子叶片 &#xff0c;251张 桃疮痂病一般&#xff0c;857张 桃疮痂病严重&#xff0c;770 张 2. GUI界面识别效果和predict.py识别效果如视频所示桃子叶片病害识别&#xff08;Python代码&#xff0c;pyTorch框架&#xff0c;深度卷积网络模型&#xff0…

探索图结构:从基础到算法应用

文章目录 理解图的基本概念学习图的遍历算法学习最短路径算法案例分析&#xff1a;使用 Dijkstra 算法找出最短路径结论 &#x1f389;欢迎来到数据结构学习专栏~探索图结构&#xff1a;从基础到算法应用 ☆* o(≧▽≦)o *☆嗨~我是IT陈寒&#x1f379;✨博客主页&#xff1a;I…

光伏+旅游景区

传统化石燃料可开发量逐渐减少&#xff0c;并且对环境造成的危害日益突出。全世界都把目光投向了可再生能源&#xff0c;希望可再生能源能够改变人类的能源结构。丰富的太阳能取之不尽、用之不竭&#xff0c;同时对环境没有影响&#xff0c;光伏发电是近些年来发展最快&#xf…

淘宝商品数据采集(如何快速获取淘宝商品信息),淘宝API接口申请指南

淘宝作为国内的电商平台&#xff0c;拥有海量的商品信息。对于想要进行淘宝商品数据采集的人来说&#xff0c;如何快速获取淘宝商品信息是一个重要的问题。本文将介绍一些快速获取淘宝商品信息的方法。 1. 使用淘宝开放平台PI 淘宝开放平台提供了多种PI接口&#xff0c;可以通…

<C++> 内存管理

1.C/C内存分布 让我们先来看看下面这段代码 int globalVar 1; static int staticGlobalVar 1; void Test() {static int staticVar 1;int localVar 1;int num1[10] {1, 2, 3, 4};char char2[] "abcd";char *pChar3 "abcd";int *ptr1 (int *) mal…

【Git】学习总结

【Git】学习总结 【一】安装【二】Git克隆项目代码【1】idea下载git项目【2】创建新的分支【3】新建的分支推送到远程【4】合并最新代码到主分支【5】切换分支 【三】提交本地项目到远程&#x1f680;1. 配置 Git&#x1f680;2. 创建项目远程仓库&#x1f680;3. 初始化本地仓…