利用c++编写bp神经网络实现手写数字识别

写在前面

从大一入学开始,本菜菜就一直想学习一下神经网络算法,但由于时间和资源所限,一直未展开比较透彻的学习。大二下人工智能课的修习,给了我一个学习的契机。现将bp神经网络的推导和实践记录于此:

前置知识

微积分相关内容,如偏导,梯度等

(大一不懂偏导梯度,这就是我学不进去的原因)

BP神经网络概况及计算方法

可以理解为一个多层的网络,包含输入层X,隐藏层H和输出层Y,其中隐藏层可以不止一层。

为了直观展示,隐藏层和输出层都被我拆成了两层进行讲解

以下为网络中每个元素的定义,及正向传播过程

\(x[1],x[2],……x[n]\)共n个点,表示输入层,所有的数据都是从该层输入。

在本手写数字识别模型中,输入的是\(28\times 28\)的灰度图,故输入层包含784个节点,值域为\([0,1]\)

第一组连边w1,为输入层X和隐藏层H之间的全连接,共\(n\times p​\)条边

其中\(w1[i][j]\)表示从输入层第i个节点,连向隐藏层第j个节点的边的边权

第二组连边w2,为隐藏层H和输出层y之间的全连边,共\(p\times q\)条边

神经网络的学习过程,即在外界输入样本的刺激下不断改变网络的连接权值,以使网络的输出不断地接近期望的输出。

在拥有足够多隐藏层神经元的情况下,两层神经网络可以拟合任意连续函数,三层神经网络可以拟合任意函数。

如果这个网络上有m个隐藏层,则总共会有m+1组连边,本代码中m=1

连边的权值是整个网络的关键所在,当连边边权确定后,即可得到一个网络

为了直观展示,隐藏层H被我拆为了两个部分进行讲解

\(hi[1],hi[2],……hi[p]\)共p个点,表示计算后的隐藏层

其中\(hi[k]=\sum\limits_{l=1}^{n}x[l]\times w1[l][k]​\)

我们也可以用矩阵乘法来表示,\(hi=x\times w1\)

\(ho[1],ho[2],……ho[p]\)共p个点,其中\(ho[i]=f(hi[i])\),\(f(x)\)表示一个激活函数

本代码中,激活函数采用\(sigmoid​\)函数

求解输出层y的过程和h的过程类似

有\(yi[k]=\sum\limits_{l=1}^{p}ho[l]\times w2[l][k]\)

有\(yo[k]=f(yi[k]),f(x)\)表示一个激活函数,本代码中y的激活函数同样采用\(sigmoid\)

在最终的输出中\(yo[k]\)的含义是:输入图片所表示的数字是k的概率为多少

正向计算代码如下:

//前向传播函数
void forward(){
//计算hi前需要对其进行清空
memset(hi,0,sizeof(hi)); //通过w1计算输入层-隐藏层输入节点
for(int i=0;i<n;i++)
for(int j=0;j<p;j++)
hi[j]+=x[i]*w1[i][j]; //通过激活函数对隐藏层进行计算
for(int i=0;i<p;i++)
ho[i]=sigmoid(hi[i]); //计算yi前需要对其进行清空
memset(yi,0,sizeof(yi)); //通过w2计算隐藏层-输出层
for(int i=0;i<p;i++)
for(int j=0;j<q;j++)
yi[j]+=ho[i]*w2[i][j]; //通过激活函数求yo
for(int i=0;i<q;i++)
yo[i]=sigmoid(yi[i]);
}

Sigmoid函数

\(Sigmoid\)函数将被用于本菜菜编写的神经网络中,其表达式如下:

\[y=sigmoid(x)=\dfrac{1}{1+e^{-x}}
\]

它可以将一个实数映射到(0,1)的区间上,在特征相差不是特别大时效果较好,其函数图形如下:

在本人实现的\(bp\)神经网络中,隐藏层的\(h\)和输出层的\(y\)都会由\(sigmoid\)函数进行处理

sigmoid函数求导

在后续求梯度的过程中,需要对sigmoid函数求导

sigmoid函数有一神器性质,即\(sigmoid'(x)=sigmoid(x)\times(1-sigmoid(x))\),推导过程如下:

\[\begin{align*}
sigmoid(x) &= \dfrac{1}{1+exp^{-x}}=\dfrac{e^x}{e^x+1}=1-(e^x+1)^{-1} \\
sigmoid'(x) &= (-1)(-1)(e^x+1)^{-2}e^x \\
&= (1+e^{-x})^{-2}e^{-2x}e^x \\
&= (1+e^{-x})^{-1}\times \dfrac{e^{-x}}{1+e^{-x}} \\
&= sigmoid(x)\times (1-sigmoid(x))
\end{align*}
\]

该公式可降低求梯度公式的计算量

我们用\(dsigmoid(y)=sigmoid'(x)\),其中\(y=sigmoid(x)\)

sigmoid和dsigmoid代码

//激活函数
double sigmoid(double x){
return 1/(1+exp(-x));
} //已知x=sigmoid(y),返回sigmoid'(y)
double dsigmoid(double x){
return x*(1-x);
}

反向传播

现在我们已经通过正向传播,求解出了对应的输出,但这个输出可能和我们的期望有所不同(如将8识别为了6)。我们要对其误差进行修正。

我们定义误差函数\(e=\frac{1}{2}\sum\limits_{k=0}^{9}(yo[k]-d[k])^2\)

其中序列\(d\)表示第\(k\)个输出节点的正确值,在本案例中,假设正确数字为\(num\),则有\(d[num]=1\),其余的\(d\)均为\(0\)

修正误差,显然就是降低e的值,而在模型中,输入是固定的,在不改动模型的情况下,提高正确率可通过修改w1和w2的权值实现。

因此,我们需要求\(\dfrac{\partial e}{\partial w1} 和 \dfrac{\partial e}{\partial w2}​\),然后借此对边权进行修正

修正w1

边权是一条一条进行修正的,假设我们要修正\(w2[i][j]\),由链式法则,修正公式如下:

\[\begin{align*}
\dfrac{\partial e}{\partial w2[i][j]} &= \dfrac{\partial e}{\partial yo[j]}\times \dfrac{\partial yo[j]}{\partial yi[j]} \times \dfrac{\partial yi[j]}{\partial w2[i][j]}
\end{align*}
\]

我们对每个模块分别求偏导,考虑\(\dfrac{\partial e}{\partial yo[j]}​\),有:

\[\begin{align*}
\dfrac{\partial e}{\partial yo[j]} &= \dfrac{\partial \bigg(\frac{1}{2}\sum\limits_{k=0}^{q-1}(yo[k]-d[k])^2 \bigg)}{\partial yo[j]} \\
&= \dfrac{\partial \bigg(\frac{1}{2}(yo[j]-d[j])^2 \bigg)}{\partial yo[j]} \\
&= \dfrac{(yo[j]-d[j])\times \partial (yo[j]-d[j]) }{\partial yo[j]} \\
&= (yo[j]-d[j])\times\dfrac{ \partial yo[j] }{\partial yo[j]} \\
&= (yo[j]-d[j])
\end{align*}
\]

在第1个等式到第2个等式,以及第3个等式到第4个等式的时候,都是将常数项进行了舍弃

考虑\(\dfrac{\partial yo[j]}{\partial yi[j]}​\),我们发现这个就是求\(sigmoid'(yi[j])​\),根据上文中的式子,我们可以得到:

$\dfrac{\partial yo[j]}{\partial yi[j]}=sigmoid(yi[j])\times (1-sigmoid(yi[j]))=dsigmoid(yo[j]) $

考虑\(\dfrac{\partial yi[j]}{\partial w2[i][j]}​\),我们将\(yi[j]​\)的求解公式代入,我们可以得到:

\[\begin{align*}
\dfrac{\partial yi[j]}{\partial w2[i][j]} &= \dfrac{\partial \bigg(\sum\limits_{k=1}^{p}ho[k]\times w2[k][j] \bigg)}{\partial w2[i][j]} \\
&= \dfrac{\partial \bigg(ho[i]\times w2[i][j] \bigg)}{\partial w2[i][j]} \\
&=ho[i]
\end{align*}
\]

综上,我们得到:

\[\dfrac{\partial e}{\partial w2[i][j]} = (yo[j]-d[j]) \times yo[j]\times (1-yo[j]) \times ho[i]
\]

那么问题来了,如何用求出来的这个公式修正w2?

其实直接令\(w2[i][j]-=\eta \times \dfrac{\partial e}{\partial w2[i][j]}\)其实就可以进行修正了,其中\(\eta​\)表示修正因子,为一个常量,本人设置为0.1

为什么可以呢?一个直观的表达为: 当误差对权值的偏导数大于零时,权值调整量为负,实际输出大于期望输出,权值向减少方向调整,使得实际输出与期望输出的差减少。当误差对权值的偏导数小于零时,权值调整量为正,实际输出小于期望输出,权值向增加方向调整,使得实际输出与期望输出的差减小。

修正w2

和w1类似,由链式法则,得:

\[\dfrac{\partial e}{\partial w1[i][j]} = \sum\limits_{k=0}^{q-1}\dfrac{\partial e}{\partial yo[k]}\times \dfrac{\partial yo[k]}{\partial yi[k]} \times \dfrac{\partial yi[k]}{\partial w2[j][k]} \times \dfrac{\partial w2[j][k]}{\partial ho[j]}\times \dfrac{\partial ho[j]}{\partial hi[j]}\times \dfrac{\partial hi[j]}{\partial w1[i][j]}
\]

为什么这里需要出现\(\sum\),主要是隐藏层节点j,可以向外连接多个输出层节点k,而这多个输出层节点k都对e会产生影响。

然后,上述式子分解步数过多,我们将\(\dfrac{\partial yi[k]}{\partial w2[j][k]} \times \dfrac{\partial w2[j][k]}{\partial ho[j]}\)进行合并,得到:

\[\dfrac{\partial e}{\partial w1[i][j]} = \sum\limits_{k=0}^{q-1}\bigg(\dfrac{\partial e}{\partial yo[k]}\times \dfrac{\partial yo[k]}{\partial yi[k]} \bigg) \times \dfrac{\partial yi[k]}{\partial ho[j]}\times \dfrac{\partial ho[j]}{\partial hi[j]}\times \dfrac{\partial hi[j]}{\partial w1[i][j]}
\]

子项\(\dfrac{\partial e}{\partial yo[k]}\times \dfrac{\partial yo[k]}{\partial yi[k]}\)在求w2的过程中已经求得,即为\((yo[k]-d[k]) \times dsigmoid(yo[k])\)

由于两层激活函数相同,子项\(\dfrac{\partial ho[j]}{\partial hi[j]}​\)等价于求w2过程中计算过的\(\dfrac{\partial yo[j]}{\partial yi[j]}​\),等于\(dsigmoid(ho[j])​\)

由于两层前向传播相同,子项\(\dfrac{\partial hi[j]}{\partial w1[i][j]}\)等价于求w2过程中计算过的\(\dfrac{\partial yi[j]}{\partial w2[i][j]}\),等于\(x[i]​\)

考虑\(\dfrac{\partial yi[k]}{\partial ho[j]}​\),我们得到:

\[\begin{align*}
\dfrac{\partial yi[k]}{\partial ho[j]} &= \dfrac{\partial \bigg( \sum\limits_{l=1}^{p} w2[l][k]\times ho[l] \bigg)}{\partial ho[j]} \\
&= \dfrac{\partial \bigg( w2[j][k]\times ho[j] \bigg)}{\partial ho[j]} \\
&= w2[j][k]
\end{align*}
\]

综上,我们得到:

\[\dfrac{\partial e}{\partial w1[i][j]} = \sum\limits_{k=1}^{q} (yo[k]-d[k]) \times dsigmoid(yo[k]) \times dsigmoid(ho[j]) \times w2[j][k] \times x[i]
\]

更新方式和w2相同

反向传播代码

严格按照上述式子编写的更新代码如下:

void back_old(){
//对w2进行更新
for(int i=0;i<p;i++)
for(int j=0;j<q;j++){
double delta=(yo[j]-d[j])*dsigmoid(yo[j])*ho[i];
w2[i][j]-=delta*learnRate;
} //对w1进行更新
for(int i=0;i<n;i++)
for(int j=0;j<p;j++){
double delta=0;
for(int k=0;k<q;k++)
delta+=(yo[k]-d[k])*dsigmoid(yo[k])*dsigmoid(ho[j])*x[i]*w2[j][k];
w1[i][j]-=delta*learnRate;
}
}

该代码的时间复杂度为\(O(npq)\),考虑对求w1偏导的代码进行优化

对求w1偏导的和式进行移项,我们得到:

\[\dfrac{\partial e}{\partial w1[i][j]} = dsigmoid(ho[j])\times x[i]\times \sum\limits_{k=1}^{q} (yo[k]-d[k]) \times dsigmoid(yo[k]) \times w2[j][k]
\]

我们开一个预处理序列W2,其中

\[W2[j] = \sum\limits_{k=1}^{q} (yo[k]-d[k]) \times dsigmoid(yo[k]) \times w2[j][k]
\]

在处理出W2后,求偏导的式子可以化简为:

\[ \dfrac{\partial e}{\partial w1[i][j]} = dsigmoid(ho[j])\times x[i]\times W2[j]
\]

则可以将时间复杂度降低到\(O(np+pq)\)

代码如下:

void back(){
//对w2进行更新
for(int i=0;i<p;i++)
for(int j=0;j<q;j++){
double delta=(yo[j]-d[j])*dsigmoid(yo[j])*ho[i];
w2[i][j]-=delta*learnRate;
} //对反向传播进行预处理
double W2[p];
memset(W2,0,sizeof(W2));
for(int j=0;j<p;j++)
for(int k=0;k<q;k++)
W2[j]+=(yo[k]-d[k])*dsigmoid(yo[k])*w2[j][k]; //对w1进行更新
for(int i=0;i<n;i++)
for(int j=0;j<p;j++){
double delta=dsigmoid(ho[j])*x[i]*W2[j];
w1[i][j]-=delta*learnRate;
}
}

Minst数据集

本bp代码所使用的数据来自该数据集

该部分将介绍所使用的的训练集数据格式,可以直接看我的代码来了解读入规则

MNIST是一个手写体数字的图片数据集,该数据集来由美国国家标准与技术研究所(National Institute of Standards and Technology (NIST))发起整理,一共统计了来自250个不同的人手写数字图片,其中50%是高中生,50%来自人口普查局的工作人员。该数据集的收集目的是希望通过算法,实现对手写数字的识别。

数据下载

官网上提供了数据集的下载链接

文件下载 文件用途
train-images-idx3-ubyte.gz 训练集图像
train-labels-idx1-ubyte.gz 训练集标签
t10k-images-idx3-ubyte.gz 测试集图像
t10k-labels-idx1-ubyte.gz 测试集标签

在上述文件中,训练集一共包含了 60,000 张图像和标签,而测试集一共包含了 10,000 张图像和标签。测试集中前5000个来自最初NIST项目的训练集.,后5000个来自最初NIST项目的测试集。前5000个比后5000个要规整,这是因为前5000个数据来自于美国人口普查局的员工,而后5000个来自于大学生。

数据集解读

下载上述四个文件后,将其解压会发现,得到的并不是一系列图片,而是 .idx1-ubyte和.idx3-ubyte 格式的文件。这是一种IDX数据格式,其基本格式如下:

magic number
size in dimension 0
size in dimension 1
size in dimension 2
.....
size in dimension N
data

其中magic number为4字节,前2字节永远是0,第3字节代表数据的格式:

0x08: unsigned byte
0x09: signed byte
0x0B: short (2 bytes)
0x0C: int (4 bytes)
0x0D: float (4 bytes)
0x0E: double (8 bytes)

第4字节的含义表示维度的数量(dimensions): 1 表示一维(比如vectors), 2 表示二维( 比如matrices),3表示三维(比如numpy表示的图像,高,宽,通道数)。

训练集和测试集的标签文件的格式(train-labels-idx1-ubyte和t10k-labels-idx1-ubyte)

idx1-ubtype的文件数据格式如下:

[offset] [type]          [value]          [description]
0000 32 bit integer 0x00000801(2049) magic number (MSB first)
0004 32 bit integer 60000 number of items
0008 unsigned byte ?? label
0009 unsigned byte ?? label
........
xxxx unsigned byte ?? label

第0 ~ 3字节,是32位整型数据,取值为0x00000801(2049),即用幻数2049记录文件数据格式,这里的格式为文本格式。

第4~7个字节,是32位整型数据,取值为60000(训练集时)或10000(测试集时),用来记录标签数据的个数;

第8个字节 ~ ),是一个无符号型的数,取值为对应0~9 之间的标签数字,用来记录样本的标签。

训练集和测试集的图像文件的格式(train-images-idx3-ubyte和t10k-images-idx3-ubyte)

idx3-ubtype的文件数据格式如下:

[offset] [type]          [value]          [description]
0000 32 bit integer 0x00000803(2051) magic number
0004 32 bit integer 60000 number of images
0008 32 bit integer 28 number of rows
0012 32 bit integer 28 number of columns
0016 unsigned byte ?? pixel
0017 unsigned byte ?? pixel
........
xxxx unsigned byte ?? pixel

即:

第0 ~ 3字节,是32位整型数据,取值为0x00000803(2051),即用幻数2051记录文件数据格式,这里的格式为图片格式。

第4~7个字节,是32位整型数据,取值为60000(训练集时)或10000(测试集时),用来记录图片数据的个数;

第8~11个字节,是32位整型数据,取值为28,用来记录图片数据的高度;

第12~15个字节,是32位整型数据,取值为28,用来记录图片数据的宽度;

第16个字节 ~ ),是一个无符号型的数,取值为0~255之间的灰度值,用来记录图片按行展开后得到的灰度值数据,其中0表示背景(白色),255表示前景(黑色)。

代码

#include<bits/stdc++.h>
using namespace std; //激活函数
double sigmoid(double x){
return 1/(1+exp(-x));
} //已知x=sigmoid(y),返回sigmoid'(y)
double dsigmoid(double x){
return x*(1-x);
} #define learnRate 0.2 //学习率
#define n 784 //输入层点个数
#define p 500 //隐藏层节点个数
#define q 10 //输出层节点个数 //与推导公式保持一致
double x[n],hi[p],ho[p],yi[q],yo[q],d[q]; //所训练的网络,采用两个矩阵表示
double w1[n][p],w2[p][q]; //随机生成一个-1到1的数
double ranDouble(){
return rand()%10/5.-1;
} void init(){
for(int i=0;i<n;i++)
for(int j=0;j<p;j++)
w1[i][j]=ranDouble(); for(int i=0;i<p;i++)
for(int j=0;j<q;j++)
w2[i][j]=ranDouble();
} //前向传播函数
void forward(){
//计算hi前需要对其进行清空
memset(hi,0,sizeof(hi)); //通过w1计算输入层-隐藏层输入节点
for(int i=0;i<n;i++)
for(int j=0;j<p;j++)
hi[j]+=x[i]*w1[i][j]; //通过激活函数对隐藏层进行计算
for(int i=0;i<p;i++)
ho[i]=sigmoid(hi[i]); //计算yi前需要对其进行清空
memset(yi,0,sizeof(yi)); //通过w2计算隐藏层-输出层
for(int i=0;i<p;i++)
for(int j=0;j<q;j++)
yi[j]+=ho[i]*w2[i][j]; //通过激活函数求yo
for(int i=0;i<q;i++)
yo[i]=sigmoid(yi[i]);
} //判断输出的答案是多少
int getAns(){
int ans=0;
for(int i=1;i<q;i++)
if(yo[i]>yo[ans]) ans=i;
return ans;
} //反向传播函数
void back_old(){
//对w2进行更新
for(int i=0;i<p;i++)
for(int j=0;j<q;j++){
double delta=(yo[j]-d[j])*dsigmoid(yo[j])*ho[i];
w2[i][j]-=delta*learnRate;
} //对w1进行更新
for(int i=0;i<n;i++)
for(int j=0;j<p;j++){
double delta=0;
for(int k=0;k<q;k++)
delta+=(yo[k]-d[k])*dsigmoid(yo[k])*dsigmoid(ho[j])*x[i]*w2[j][k];
w1[i][j]-=delta*learnRate;
}
} //反向传播函数
void back(){
//对w2进行更新
for(int i=0;i<p;i++)
for(int j=0;j<q;j++){
double delta=(yo[j]-d[j])*dsigmoid(yo[j])*ho[i];
w2[i][j]-=delta*learnRate;
} //对反向传播进行预处理
double W2[p];
memset(W2,0,sizeof(W2));
for(int j=0;j<p;j++)
for(int k=0;k<q;k++)
W2[j]+=(yo[k]-d[k])*dsigmoid(yo[k])*w2[j][k]; //对w1进行更新
for(int i=0;i<n;i++)
for(int j=0;j<p;j++){
double delta=dsigmoid(ho[j])*x[i]*W2[j];
w1[i][j]-=delta*learnRate;
}
} FILE *fImg,*fAns; int last[100000]; void train(int cas){
//读入一张新的图片
//除了前16字节,接下来的信息都是一张一张的图片
//每张图片大小为28*28,每个char表示该像素对应的灰度,范围为0至255
unsigned char img[n],num;
fread(img,1,n,fImg);
for(int i=0;i<n;i++) x[i]=img[i]/255.; //读入该图片对应的答案
//除了前8字节,第k个字节对应第k张图片的正确答案
fread(&num,1,1,fAns);
for(int i=0;i<q;i++) d[i]=(num==i); forward();
int ans=getAns();
last[cas]=(num==ans);
back(); //输出最近100局的正确率
int P=100,hh=0;
if(cas%P==0){
for(int i=0;i<P;i++)
hh+=last[cas-i];
printf("%d %d\n",cas,hh);
}
} //int imgs=16,nums=8;
int main(){
fImg=fopen("train-images.idx3-ubyte","rb");
fseek(fImg,16,SEEK_SET);
fAns=fopen("train-labels.idx1-ubyte","rb");
fseek(fAns,8,SEEK_SET); init();
for(int cas=1;cas<=60000;cas++){
train(cas);
} }

训练效果

在隐藏层点数设置为500时,完成60000次训练,需要21s

最后2500次训练的正确率如图所示,平均约为97%:

训练次数-正确率曲线如图所示:

利用c++编写bp神经网络实现手写数字识别详解的更多相关文章

  1. BP神经网络的手写数字识别

    BP神经网络的手写数字识别 ANN 人工神经网络算法在实践中往往给人难以琢磨的印象,有句老话叫“出来混总是要还的”,大概是由于具有很强的非线性模拟和处理能力,因此作为代价上帝让它“黑盒”化了.作为一种 ...

  2. 【机器学习】BP神经网络实现手写数字识别

    最近用python写了一个实现手写数字识别的BP神经网络,BP的推导到处都是,但是一动手才知道,会理论推导跟实现它是两回事.关于BP神经网络的实现网上有一些代码,可惜或多或少都有各种问题,在下手写了一 ...

  3. BP神经网络(手写数字识别)

    1实验环境 实验环境:CPU i7-3770@3.40GHz,内存8G,windows10 64位操作系统 实现语言:python 实验数据:Mnist数据集 程序使用的数据库是mnist手写数字数据 ...

  4. NN:利用深度学习之神经网络实现手写数字识别(数据集50000张图片)—Jason niu

    import mnist_loader import network training_data, validation_data, test_data = mnist_loader.load_dat ...

  5. TensorFlow卷积神经网络实现手写数字识别以及可视化

    边学习边笔记 https://www.cnblogs.com/felixwang2/p/9190602.html # https://www.cnblogs.com/felixwang2/p/9190 ...

  6. 卷积神经网络CNN 手写数字识别

    1. 知识点准备 在了解 CNN 网络神经之前有两个概念要理解,第一是二维图像上卷积的概念,第二是 pooling 的概念. a. 卷积 关于卷积的概念和细节可以参考这里,卷积运算有两个非常重要特性, ...

  7. 第二节,TensorFlow 使用前馈神经网络实现手写数字识别

    一 感知器 感知器学习笔记:https://blog.csdn.net/liyuanbhu/article/details/51622695 感知器(Perceptron)是二分类的线性分类模型,其输 ...

  8. TensorFlow.NET机器学习入门【5】采用神经网络实现手写数字识别(MNIST)

    从这篇文章开始,终于要干点正儿八经的工作了,前面都是准备工作.这次我们要解决机器学习的经典问题,MNIST手写数字识别. 首先介绍一下数据集.请首先解压:TF_Net\Asset\mnist_png. ...

  9. 基于卷积神经网络的手写数字识别分类(Tensorflow)

    import numpy as np import tensorflow as tf from tensorflow.examples.tutorials.mnist import input_dat ...

随机推荐

  1. Mysql事物锁等待超时(Lock wait timeout exceeded; try restarting transaction)

    一.问题描述 在做查询语句时,MySQL 抛出了这样的异常:锁等待超时 Lock wait timeout exceeded; try restarting transaction,是当前事务在等待其 ...

  2. python带你采集不可言说网站数据,并带你多重骚操作~

    前言 嗨喽,大佬们好鸭!这里是小熊猫~ 今天我们采集国内知名的shipin弹幕网站! 这里有及时的动漫新番,活跃的ACG氛围,有创意的Up主. 大家可以在这里找到许多欢乐. 目录(可根据个人情况点击你 ...

  3. CF141E Clearing Up 题解

    思路分析 自认为是一道很好的思维题. 直接看上去的想法是: 跑一个生成树,每一次加的边颜色交替进行,直到拉出生成树. 仔细想想,发现可能无法保证最后是一棵树而不是森林,也是说输出都是 \(-1\) . ...

  4. Transferable Joint Attribute-Identity Deep Learning for Unsupervised Person Re-Identification理解

    简介:这篇文章属于跨域无监督行人再识别,不同于大部分文章它使用了属性标注.旨在于能够学习到有属性语义与有区分力的身份特征的表达空间(TJ-AIDL),并能够转移到一个没有看到过的域. 贡献: 提出了一 ...

  5. 《A Neural Algorithm of Artistic Style》理解

    在美术中,特别是绘画,人类掌握了通过在图像的内容和风格间建立复杂的相互作用从而创造独特的视觉体验的技巧.到目前为止,这个过程的算法基础是未知的,也没有现存的人工系统拥有这样的能力.然而在视觉感知的其他 ...

  6. 字符输出流_Writer类&FileWriter类介绍和字符输出流的基本使用_写出单个字符到文件

    java.io.Writer:字符输出流,是所有字符输出流的最顶层的父类,是一个抽象类 共性的成员方法: - void write(int c) 写入单个字符 - void write(char[] ...

  7. Collections集合工具类的方法_sort(List)和sort(List,Comparator)方法

    Comparator比较器 我们还是先研究这个方法 public static <T> void sort(List<T> list):将集合中元素按照默认规则排序. 不过这次 ...

  8. 4-10 CS后台项目练习-3 || Redis

    13. 类别管理--根据id查询类别详情--持久层 13.1. 规划SQL语句 本次需要执行的SQL语句大致是: select * from pms_category where id=? 关于字段列 ...

  9. Solution -「BZOJ3894」文理分科

    Sol. 说实话,对于一个初学者,这道题很难看出是一道网络流-最小割.对于一个熟练者,这是比较套路的一种模型. 最小割,可以看做是在一个图中删掉最小的边权和使得源点.汇点不连通.或者换一个角度,可以看 ...

  10. Solution -「基环树」做题记录

    写的大多只是思路,比较简单的细节和证明过程就不放了,有需者自取. 基环树简介 简单说一说基环树吧.由名字扩展可得这是一类以环为基础的树(当然显然它不是树. 通常的表现形式是一棵树再加一条非树边,把图画 ...