Java实现手写数字的识别(BP神经网络的运用)
最近对机器学习方面的知识有点感兴趣,所以特别的对神经网络方面的知识进行了了解。然而,发现大部分的人都是通过Python或者R语言及其Matlab来进行实验的,而自己却还没有时间进行学习,而且对Java语言有一种情有独钟的感觉,所以特别的就用Java语言实现BP神经网络。PS:这个内容其实在Python和Matlab中都有已经封装好的库,直接调用就可以了的,而且效率也还不错。而我只是尝试试着用Java进行写写而已(最后的效果还不如已经封装好的代码),自己并没有进行过多的优化。
下面就介绍一个简单的例子,就是对手写的数字通过BP神经网络进行识别,我也刚接触机器学习,所以有不足的地方欢迎大家进行指正,共同学习。
在这里就不再对BP神经网络的原理进行过多的阐述了,如果大家对于这个方面不懂的话,可以自行百度的进行了解。
例子简介:
该问题解决的是把28x28像素的灰度手写数字图片识别为相应的数字,其中数字的范围从0到9.
训练样本:训练集中包括60000个样本,因为所有数据集中28x28像素的灰度图片的尺寸为784,所以训练
集输出的格式为[60000, 784]。
一:训练数据和测试数据的格式
训练数据的格式如图所示:
这只是部分显示的效果,在这里进行讲解一下:其中每一行一共是784个输入,因为这是由一个28*28的图片进行像素转换过来的,而且每一列的数值都是在0-255之间。
标签结果:
当然,这也只是部分训练集的结果,其对应的就是上面训练集的识别结果,所以主要的作用也就是标签。
二:实验思路
(1):构建神经网络层次结构
由训练集数据可知,手写输入的数据维数为784维,而对应的输出结果为分别为0-9的10个数字,所以根据训练集的数据可知,在构建的神经网络的输入层的神经元的节点个数为784个,而对应的输出层的神经元个数为10个。由于在神经网络的隐层可以有一层或者多层,对此自己在本实验中,采取的是只采用了一层隐层结构。
(2):确定隐层中的神经元的个数
因为对于隐层的神经元个数的确定目前还没有什么比较完美的解决方案,所以对此经过自己查阅书籍和上网查阅资料,有以下的几种经验方式来确定隐层的神经元的个数,方式分别如下所示:
1) 一般取(输入+输出)/2
2) 隐层一般小于输入层
3)(输入层+1)/2
4) log(输入层)
5) log(输入层)+10
由于上述的也是由经验而得来的,所以自己在实验过程中分别的测试了一下,最后得到以第五种的方式得到的测试结果相对较高。
(3):设置神经元的激活函数
在《机器学习》的书中介绍了两种比较常用的函数,分别是阶跃函数和Sigmoid函数。最后自己采用了后者函数。
(4):初始化输入层和隐层之间神经元间的权值信息
采用的是使用简单的随机数分配的方法,并且两层之间的神经元权值是通过二维数组进行保留,数组的索引就代表着两层对应的神经元的索引信息
(5):初始化隐层和输出层之间神经元间的权值信息
采用的是使用简单的随机数分配的方法,并且两层之间的神经元权值是通过二维数组进行保留,数组的索引就代表着两层对应的神经元的索引信息
(6):读取CSV测试集表格信息,并加载到程序用数据保存,其中将每个维数的数据都换成了0和1的二进制数进行处理。
(7):读取CSV测试集结果表格信息,并加载到程序用数据保存
(8):计算输入层与隐层中隐层神经元的阈值
这里主要是采用了下面的方法:
Sum=sum+weight[i][j] * layer0[i];
参数的含义:将每个输入层中的神经元与神经元的权值信息weight[i][j]乘以对应的输入层神经元的阈值累加,然后再调用激活函数得到对应的隐层神经元的阈值。
(9):计算隐层与输出层中输出层的神经元的阈值
方法和上面的类似,只是相对应的把权值信息进行了修改即可。
(10):计算误差逆传播(输出层的逆误差)
采用书上P103页的方法(西瓜书)
(11):计算误差传播(隐层的逆误差)
采用书上P103页的方法(西瓜书)
(12):更新各层神经元之间的权值信息
double newVal = momentum * prevWeight[j][i] + eta * delta[i] * layer[j];
参数:其中设置momentum 为0.9,设置eta 为0.25,prevWeight[j][i]表示神经元之间的权值,layer[j]和delta[i]表示两层不同神经元的阈值。
(13):循环迭代训练5次
(14):输入测试集数据
(15):输出测试集预测结果和实际结果进行比较,得到精确度
好了,上面就是我大概的思路了,其实是不是很简单呢。当然这只是最基本的而已,因为我并没有进行优化,以至于后面的识别效果并不是很好。
预测结果:,最后的精确度平均值在85%左右(并不理想)。
虽然结果不是很好,但是其BP的原理还是实现了,此处鼓励鼓励。
好了,不多说了,把代码直接贴出来。(其中由于训练数据和测试数据都是CSV格式,所以在用Java时需要进行特别的读取处理。)
1)读取CSV格式的文件:
package shenjingwangluo2;/** 读取后缀为csv的excell文件* */import java.io.BufferedReader;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;public class CSVFileUtil {private String fileName = null;private BufferedReader br = null;private List<String> list = new ArrayList<String>();public CSVFileUtil() {}public CSVFileUtil(String fileName) throws Exception {this.fileName = fileName;br = new BufferedReader(new FileReader(fileName));String stemp;while ((stemp = br.readLine()) != null) {list.add(stemp);}}public List getList() {return list;}/*** 获取行数* @return*/public int getRowNum() {return list.size();}/*** 获取列数* @return*/public int getColNum() {if (!list.toString().equals("[]")) {if (list.get(0).toString().contains(",")) {// csv为逗号分隔文件return list.get(0).toString().split(",").length;} else if (list.get(0).toString().trim().length() != 0) {return 1;} else {return 0;}} else {return 0;}}/*** 获取制定行* @param index* @return*/public String getRow(int index) {if (this.list.size() != 0) {return (String) list.get(index);} else {return null;}}/*** 获取指定列* @param index* @return*/public String getCol(int index) {if (this.getColNum() == 0) {return null;}StringBuffer sb = new StringBuffer();String tmp = null;int colnum = this.getColNum();if (colnum > 1) {for (Iterator it = list.iterator(); it.hasNext();) {tmp = it.next().toString();sb = sb.append(tmp.split(",")[index] + ",");}} else {for (Iterator it = list.iterator(); it.hasNext();) {tmp = it.next().toString();sb = sb.append(tmp + ",");}}String str = new String(sb.toString());str = str.substring(0, str.length() - 1);return str;}/*** 获取某个单元格* @param row* @param col* @return*/public String getString(int row, int col) {String temp = null;int colnum = this.getColNum();if (colnum > 1) {temp = list.get(row).toString().split(",")[col];} else if(colnum == 1){temp = list.get(row).toString();} else {temp = null;}return temp;}public void CsvClose()throws Exception{this.br.close();}}
2)BP神经网络构建
package shenjingwangluo2;import java.util.Random;public class BP {private final double[] input; //输入层private final double[] hidden; //隐含层private final double[] output; //输出层private final double[] target; //预测的输出内容private final double[] hidDelta; //隐含层的神经元的误差(每一个的)private final double[] optDelta; //输出层的神经元的误差(每一个的)private final double eta; //学习率private final double momentum; //动量参数private final double[][] iptHidWeights; //从输入层到隐含层的矩阵权值private final double[][] hidOptWeights; //从隐含层到输出层的矩阵权值private final double[][] iptHidPrevUptWeights; //更新之前的权值信息(输入层到隐含层)private final double[][] hidOptPrevUptWeights; //更细之前的权值信息(隐含层到输出层)public double optErrSum = 0d; public double hidErrSum = 0d; private final Random random; //主要是对权值采取的是随机产生的方法//初始化public BP(int inputSize, int hiddenSize, int outputSize, double eta, double momentum) { input = new double[inputSize + 1]; hidden = new double[hiddenSize + 1]; output = new double[outputSize + 1]; target = new double[outputSize + 1]; hidDelta = new double[hiddenSize + 1]; optDelta = new double[outputSize + 1]; iptHidWeights = new double[inputSize + 1][hiddenSize + 1]; hidOptWeights = new double[hiddenSize + 1][outputSize + 1]; random = new Random(100000); //使每次产生的随机数都是第一次的分配,这是有参数和没参数的区别randomizeWeights(iptHidWeights); //分配输入层到隐含层的神经元的权值randomizeWeights(hidOptWeights); //分配隐含层到输出层的神经元的权值iptHidPrevUptWeights = new double[inputSize + 1][hiddenSize + 1]; //更新权值hidOptPrevUptWeights = new double[hiddenSize + 1][outputSize + 1]; this.eta = eta; //学习率this.momentum = momentum; //动态量 } //随机产生神经元之间的权值信息private void randomizeWeights(double[][] matrix) { for (int i = 0, len = matrix.length; i != len; i++) for (int j = 0, len2 = matrix[i].length; j != len2; j++) { double real = random.nextDouble(); //随机分配着产生0-1之间的值matrix[i][j] = random.nextDouble() > 0.5 ? real : -real; } } //初始化输入层,隐含层,和输出层public BP(int inputSize, int hiddenSize, int outputSize) { this(inputSize, hiddenSize, outputSize, 0.25, 0.9); } public void train(double[] trainData, double[] target) { //训练数据loadInput(trainData); //加载输入的数据loadTarget(target); //加载输出的结果数据forward(); //向前计算神经元权值(先算输入到隐含层的,然后再算隐含到输出层的权值)calculateDelta(); //计算误差逆传播值 adjustWeight(); //调整更新神经元的权值} //测试自己弄的BP神经网络训练的效果咋样 public double[] test(double[] inData) { if (inData.length != input.length - 1) { throw new IllegalArgumentException("Size Do Not Match."); } System.arraycopy(inData, 0, input, 1, inData.length); forward(); return getNetworkOutput(); } //返回最后的输出层的结果 private double[] getNetworkOutput() { int len = output.length; double[] temp = new double[len - 1]; for (int i = 1; i != len; i++) temp[i - 1] = output[i]; return temp; } //将之前那些训练数据的结果加载进来,存放,方便训练private void loadTarget(double[] arg) { if (arg.length != target.length - 1) { throw new IllegalArgumentException("Size Do Not Match."); } System.arraycopy(arg, 0, target, 1, arg.length); //方法和之前的输入数据一样,都是调用复制数据的方法} //加载训练数据private void loadInput(double[] inData) { if (inData.length != input.length - 1) { throw new IllegalArgumentException("Size Do Not Match."); } System.arraycopy(inData, 0, input, 1, inData.length); //调用系统复制数组的方法(存放输入的训练数据)} //向前计算各个神经元的权值 (输入层到隐含层的)(参数一:输入层的数据,二:隐含层的内容,三:输入到隐含的神经元的权值)//向前计算各个神经元的权值 (隐含层到输出层)(参数一:隐含层的数据,二:输出层的内容,三:隐含层到输出层的神经元的权值)private void forward(double[] layer0, double[] layer1, double[][] weight) { layer0[0] = 1.0; for (int j = 1, len = layer1.length; j != len; ++j) { double sum = 0; //保存权值 for (int i = 0, len2 = layer0.length; i != len2; ++i) sum += weight[i][j] * layer0[i]; layer1[j] = sigmoid(sum); //调用神经元的激活函数来得到结果(结果肯定是在0-1之间的)} } //向前计算(先算输入到隐含层的,然后再算隐含到输出层的权值)private void forward() { forward(input, hidden, iptHidWeights); //计算输入层到隐含层的权值forward(hidden, output, hidOptWeights); //计算隐含层到输出层的权值} //计算输出层的误差private void outputErr() { double errSum = 0; //误传播值for (int idx = 1, len = optDelta.length; idx != len; ++idx) { double o = output[idx]; optDelta[idx] = o * (1d - o) * (target[idx] - o); //书上p104的公式errSum += Math.abs(optDelta[idx]); } optErrSum = errSum; } //计算隐含层的误差private void hiddenErr() { double errSum = 0; //保存误差 for (int j = 1, len = hidDelta.length; j != len; ++j) { double o = hidden[j]; //神经元权值double sum = 0; for (int k = 1, len2 = optDelta.length; k != len2; ++k) //由输出层来反向计算sum += hidOptWeights[j][k] * optDelta[k]; hidDelta[j] = o * (1d - o) * sum; //书上的P104的(5.15)公式errSum += Math.abs(hidDelta[j]); } hidErrSum = errSum; } //计算每一层的误差(因为在BP中,要达到使误差最小)(就是逆传播算法,书上有P101)private void calculateDelta() { outputErr(); //计算输出层的误差(因为要反过来算,所以先算输出层的)hiddenErr(); //计算隐含层的误差} //更新每层中的神经元的权值信息(这也就是不断的训练过程,) private void adjustWeight(double[] delta, double[] layer, double[][] weight, double[][] prevWeight) { layer[0] = 1; for (int i = 1, len = delta.length; i != len; ++i) { for (int j = 0, len2 = layer.length; j != len2; ++j) { double newVal = momentum * prevWeight[j][i] + eta * delta[i] * layer[j]; //通过公式计算误差限=(动态量*之前的该神经元的阈值+学习率*误差*对应神经元的阈值),来进行更新权值 weight[j][i] += newVal; //得到新的神经元之间的权值prevWeight[j][i] = newVal; //保存这一次得到的权值,方便下一次进行更新} } } //更新每层中的神经元的权值信息private void adjustWeight() { adjustWeight(optDelta, hidden, hidOptWeights, hidOptPrevUptWeights); adjustWeight(hidDelta, input, iptHidWeights, iptHidPrevUptWeights); } //我这里用的是sigmoid激活函数,当然也可以用阶跃函数,看自己选择吧 private double sigmoid(double val) { return 1d / (1d + Math.exp(-val)); //这函数书上有P98页} }
3)进行训练和测试
package shenjingwangluo2;import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Random;
import java.util.Scanner;import javax.xml.crypto.Data;public class Test {public static void main(String[] args) throws Exception { Date data=new Date();DateFormat df=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");System.out.println(df.format(data));BP bp = new BP(784, 30, 10); //分别代表784个输入层,一层隐含层其中是784个神经元,10个输出结果 //得到训练数据的结果// File fileresult=new File("D:\\text.et"); //训练数据的结果CSVFileUtil util = new CSVFileUtil("D:\\text.csv");int resulthang=util.getRowNum(); //得到训练结果行数int resultlie=util.getColNum(); //得到训练结果列数// String[][] numberresult=GetNumberInfo.getData(fileresult, 0);//得到训练数据的结果
// File inputdata=new File("D:\\traindata.et"); //训练数据的784个像素CSVFileUtil util2 = new CSVFileUtil("D:\\traindata.csv");int inputhang=util2.getRowNum(); //得到训练结果行数int inputlie=util2.getColNum(); //得到训练结果列数// String[][] numberxunlian=GetNumberInfo.getData(inputdata, 0); for(int i=0;i<20;i++){ //训练迭代次数for(int index=0;index<resulthang;index++){double[] xunlianresult=new double[10]; //因为结果有0-9这10种情况String getresult=util.getString(index, 0); //得到训练集的每个的结果xunlianresult[Integer.parseInt(getresult)]=1; //表示当前的这个训练集的结果是对应的下标的值//将训练数据的每一个转换成只有0和1的形式double[] binary = new double[784]; //转成二进制进行处理(784位)int suoyinlie = 0; int value=0;while(suoyinlie<784){value=Integer.parseInt(util2.getString(index, suoyinlie));if(value>=255/2){binary[suoyinlie]=1; //主要是为了让数据中只有0和1这样的灰度数据方便计算}else{binary[suoyinlie]=0;}suoyinlie++;}bp.train(binary,xunlianresult); //训练数据}}/*/之前测试用的//输入层的训练数据Random random = new Random(); List<Integer> list = new ArrayList<Integer>(); //存取1000个随机数(主要是用来当输入层数据的)for (int i = 0; i <= 1000; i++) { int value = random.nextInt(); list.add(value); } for (int i = 0; i <= 200; i++) { for (int value : list) { //取训练数据double[] real = new double[4]; if (value >= 0) //随机数是正数if ((value & 1) == 1) //正奇数real[0] = 1; else //正偶数real[1] = 1; else if ((value & 1) == 1) //负奇数 real[2] = 1; else //负偶数real[3] = 1; double[] binary = new double[32]; //转成二进制进行处理int index = 31; do { binary[index--] = (value & 1); value >>>= 1; } while (value != 0); bp.train(binary, real); //训练数据} } */System.out.println("神经网络拓扑结构训练好了,可以进行测试");computeTextData(bp);}/*while (true) { byte[] input = new byte[10]; System.in.read(input); Integer value = Integer.parseInt(new String(input).trim()); //输入测试数据Scanner sc=new Scanner(System.in);int value=sc.nextInt();int rawVal = value; double[] binary = new double[32]; int index = 31; do { binary[index--] = (value & 1); value >>>= 1; } while (value != 0); double[] result = bp.test(binary); double max = -Integer.MIN_VALUE; int idx = -1; for (int i = 0; i != result.length; i++) { if (result[i] > max) { //得到输出层中最大的权值,就可以得到它的属性是哪个数字了max = result[i]; idx = i; //保存是属于哪一个输出层的数值,也就是代表它属于哪一类 } } switch (idx) { case 0: System.out.format("%d是一个正奇数\n", rawVal); break; case 1: System.out.format("%d是一个正偶数\n", rawVal); break; case 2: System.out.format("%d是一个负奇数\n", rawVal); break; case 3: System.out.format("%d是一个负偶数\n", rawVal); break; } } }*///输出测试的结果private static void computeTextData(BP bp) throws Exception {
// File textinputdata=new File("D:\\textdata.et");
// String[][] textdata=GetNumberInfo.getData(textinputdata, 0); //得到测试数据CSVFileUtil util3 = new CSVFileUtil("D:\\textdata.csv");int textthang=util3.getRowNum(); //得到测试数据行数int textlie=util3.getColNum(); //得到测试数据列数CSVFileUtil util4 = new CSVFileUtil("D:\\textresult.csv");int textresultthang=util4.getRowNum(); //得到测试结果行数int textresultlie=util4.getColNum(); //得到测试结果列数int textrightnumber=0; //得到预测和实际结果相同的个数(计算正确率)for(int hanggeshu=0;hanggeshu<textthang;hanggeshu++){double[] binary = new double[784]; //转成二进制进行处理(784位)int suoyinlie = 0; int value=0;while(suoyinlie<784){value=Integer.parseInt(util3.getString(hanggeshu, suoyinlie));if(value>=255/2){binary[suoyinlie]=1; //主要是为了让数据中只有0和1这样的灰度数据方便计算}else{binary[suoyinlie]=0;}suoyinlie++;}double[] result = bp.test(binary); //测试的结果double max = result[0]; int idx = -1; for(int a=0;a<10;a++){}for (int i = 0; i <result.length; i++) { if (result[i] > max) { //得到输出层中最大的权值,就可以得到它的属性是哪个数字了max = result[i]; idx = i; //保存是属于哪一个输出层的数值,也就是代表它属于哪一类 } }if(idx==Integer.parseInt(util4.getString(hanggeshu,0))){ //计算正确率textrightnumber++;}switch (idx) {case 0:System.out.println("测试结果是0");break;case 1:System.out.println("测试结果是1");break;case 2:System.out.println("测试结果是2"); break;case 3:System.out.println("测试结果是3");break;case 4:System.out.println("测试结果是4");break;case 5:System.out.println("测试结果是5");break;case 6:System.out.println("测试结果是6");break;case 7:System.out.println("测试结果是7");break;case 8:System.out.println("测试结果是8");break;case 9:System.out.println("测试结果是9");break;default:break;}}Date data2=new Date();DateFormat df=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");System.out.println(df.format(data2)); System.out.println("正确率为:"+(double)(textrightnumber)/textresultthang);} }
好了,这就是大概的一个过程,其中参考了网上的一些博客资源,再次感谢,欢迎交流。
下面把数据的链接贴出来(百度云盘)。
https://pan.baidu.com/s/1boFR2Bd
Java实现手写数字的识别(BP神经网络的运用)相关推荐
- linux手写数字识别opencv,opencv实现KNN手写数字的识别
人工智能是当下很热门的话题,手写识别是一个典型的应用.为了进一步了解这个领域,我阅读了大量的论文,并借助opencv完成了对28x28的数字图片(预处理后的二值图像)的识别任务. 预处理一张图片: 首 ...
- DL之CNN:利用卷积神经网络算法(2→2,基于Keras的API-Functional)利用MNIST(手写数字图片识别)数据集实现多分类预测
DL之CNN:利用卷积神经网络算法(2→2,基于Keras的API-Functional)利用MNIST(手写数字图片识别)数据集实现多分类预测 目录 输出结果 设计思路 核心代码 输出结果 下边两张 ...
- DL之CNN:利用卷积神经网络算法(2→2,基于Keras的API-Sequential)利用MNIST(手写数字图片识别)数据集实现多分类预测
DL之CNN:利用卷积神经网络算法(2→2,基于Keras的API-Sequential)利用MNIST(手写数字图片识别)数据集实现多分类预测 目录 输出结果 设计思路 核心代码 输出结果 1.10 ...
- DL之DNN:利用DNN【784→50→100→10】算法对MNIST手写数字图片识别数据集进行预测、模型优化
DL之DNN:利用DNN[784→50→100→10]算法对MNIST手写数字图片识别数据集进行预测.模型优化 导读 目的是建立三层神经网络,进一步理解DNN内部的运作机制 目录 输出结果 设计思路 ...
- Dataset之Handwritten Digits:Handwritten Digits(手写数字图片识别)数据集简介、安装、使用方法之详细攻略
Dataset之Handwritten Digits:Handwritten Digits(手写数字图片识别)数据集简介.安装.使用方法之详细攻略 目录 Handwritten Digits数据集的简 ...
- TF之NN:利用DNN算法(SGD+softmax+cross_entropy)对mnist手写数字图片识别训练集(TF自带函数下载)实现87.4%识别
TF之NN:利用DNN算法(SGD+softmax+cross_entropy)对mnist手写数字图片识别训练集(TF自带函数下载)实现87.4%识别 目录 输出结果 代码设计 输出结果 代码设计 ...
- TF:基于CNN(2+1)实现MNIST手写数字图片识别准确率提高到99%
TF:基于CNN(2+1)实现MNIST手写数字图片识别准确率提高到99% 导读 与Softmax回归模型相比,使用两层卷积的神经网络模型借助了卷积的威力,准确率高非常大的提升. 目录 输出结果 代码 ...
- TF:利用是Softmax回归+GD算法实现MNIST手写数字图片识别(10000张图片测试得到的准确率为92%)
TF:利用是Softmax回归+GD算法实现MNIST手写数字图片识别(10000张图片测试得到的准确率为92%) 目录 设计思路 全部代码 设计思路 全部代码 #TF:利用是Softmax回归+GD ...
- Dataset之MNIST:MNIST(手写数字图片识别+ubyte.gz文件)数据集的下载(基于python语言根据爬虫技术自动下载MNIST数据集)
Dataset之MNIST:MNIST(手写数字图片识别+ubyte.gz文件)数据集的下载(基于python语言根据爬虫技术自动下载MNIST数据集) 目录 数据集下载的所有代码 1.主文件 mni ...
最新文章
- 用python做一个图片验证码
- socket的三次握手
- JavaScript几个小技巧
- nginx域名配置虚拟主机
- Java教程之JDBC中的常用接口和类
- SAP CDS view 单元测试框架 Test Double 介绍
- linux 文件按照大小排列
- 怎么用源程序把ChemDraw结构复制到Word文档
- 解决微信小程序 [Component] slot ““ is not found.
- IPv6 OSPFv3路由协议(续二)
- Java工作笔记-类型转换的一种思路(前后端分离、反射)
- Pycharm中如何pip下载包更快
- 川大计算机专业导师冯子亮,川大计算机学院硕导名单_跨考网
- mysql版本在哪看_mysql版本查询(mysql版本有哪些)
- 邻接矩阵/图/DFS/BFS
- 电影先生搭建影视网站教程
- 如何从服务器复制cadence库文件到windows,cadence使用笔记
- windows7利用EasyBCD安装centos6.3双系统
- 【Dubbo】Dubbo2.7新功能 --配置中心、元数据中心
- p12解析流程_OpenSSL 解析P12格式证书文件
热门文章
- Android应用APP脱壳笔记
- 利用poi 1.6 通过模板导出复杂 Word表格,包含合单元格后垂直居中、循环生成行等
- react之redux全局状态管理
- 虚拟机开启及简单的系统命令
- 计算机网络课外实验一级 20175319江野
- 【Tomcat】tomcat9使用tomcat7-maven-plugin插件配置及其他一系列配置问题解决
- C++高阶 RAII机制(以对象管理资源)
- php模拟苹果手机访问,php 使用curl模拟ip和来源进行访问的实现方法
- adobe cs5全线作品
- 深入理解计算机系统-之-数值存储(二)--C程序打印变量的每一字节或者位