CNN卷积神经网络

代码的注释:

cnnexamples.m

[plain]  view plain copy
  1. clear all; close all; clc;  
  2. addpath('../data');  
  3. addpath('../util');  
  4. load mnist_uint8;  
  5.   
  6. train_x = double(reshape(train_x',28,28,60000))/255;  
  7. test_x = double(reshape(test_x',28,28,10000))/255;  
  8. train_y = double(train_y');  
  9. test_y = double(test_y');  
  10.   
  11. %% ex1   
  12. %will run 1 epoch in about 200 second and get around 11% error.   
  13. %With 100 epochs you'll get around 1.2% error  
  14.   
  15. cnn.layers = {  
  16.     struct('type', 'i') %input layer  
  17.     struct('type', 'c', 'outputmaps', 6, 'kernelsize', 5) %convolution layer  
  18.     struct('type', 's', 'scale', 2) %sub sampling layer  
  19.     struct('type', 'c', 'outputmaps', 12, 'kernelsize', 5) %convolution layer  
  20.     struct('type', 's', 'scale', 2) %subsampling layer  
  21. };  
  22.   
  23. % 这里把cnn的设置给cnnsetup,它会据此构建一个完整的CNN网络,并返回  
  24. cnn = cnnsetup(cnn, train_x, train_y);  
  25.   
  26. % 学习率  
  27. opts.alpha = 1;  
  28. % 每次挑出一个batchsize的batch来训练,也就是每用batchsize个样本就调整一次权值,而不是  
  29. % 把所有样本都输入了,计算所有样本的误差了才调整一次权值  
  30. opts.batchsize = 50;   
  31. % 训练次数,用同样的样本集。我训练的时候:  
  32. % 1的时候 11.41% error  
  33. % 5的时候 4.2% error  
  34. % 10的时候 2.73% error  
  35. opts.numepochs = 10;  
  36.   
  37. % 然后开始把训练样本给它,开始训练这个CNN网络  
  38. cnn = cnntrain(cnn, train_x, train_y, opts);  
  39.   
  40. % 然后就用测试样本来测试  
  41. [er, bad] = cnntest(cnn, test_x, test_y);  
  42.   
  43. %plot mean squared error  
  44. plot(cnn.rL);  
  45. %show test error  
  46. disp([num2str(er*100) '% error']);  


cnnsetup.m

[plain]  view plain copy
  1. function net = cnnsetup(net, x, y)  
  2.     inputmaps = 1;  
  3.     % B=squeeze(A) 返回和矩阵A相同元素但所有单一维都移除的矩阵B,单一维是满足size(A,dim)=1的维。  
  4.     % train_x中图像的存放方式是三维的reshape(train_x',28,28,60000),前面两维表示图像的行与列,  
  5.     % 第三维就表示有多少个图像。这样squeeze(x(:, :, 1))就相当于取第一个图像样本后,再把第三维  
  6.     % 移除,就变成了28x28的矩阵,也就是得到一幅图像,再size一下就得到了训练样本图像的行数与列数了  
  7.     mapsize = size(squeeze(x(:, :, 1)));  
  8.   
  9.     % 下面通过传入net这个结构体来逐层构建CNN网络  
  10.     % n = numel(A)返回数组A中元素个数  
  11.     % net.layers中有五个struct类型的元素,实际上就表示CNN共有五层,这里范围的是5  
  12.     for l = 1 : numel(net.layers)   %  layer  
  13.         if strcmp(net.layers{l}.type, 's') % 如果这层是 子采样层  
  14.             % subsampling层的mapsize,最开始mapsize是每张图的大小28*28  
  15.             % 这里除以scale=2,就是pooling之后图的大小,pooling域之间没有重叠,所以pooling后的图像为14*14  
  16.             % 注意这里的右边的mapsize保存的都是上一层每张特征map的大小,它会随着循环进行不断更新  
  17.             mapsize = floor(mapsize / net.layers{l}.scale);  
  18.             for j = 1 : inputmaps % inputmap就是上一层有多少张特征图  
  19.                 net.layers{l}.b{j} = 0; % 将偏置初始化为0  
  20.             end  
  21.         end  
  22.         if strcmp(net.layers{l}.type, 'c') % 如果这层是 卷积层  
  23.             % 旧的mapsize保存的是上一层的特征map的大小,那么如果卷积核的移动步长是1,那用  
  24.             % kernelsize*kernelsize大小的卷积核卷积上一层的特征map后,得到的新的map的大小就是下面这样  
  25.             mapsize = mapsize - net.layers{l}.kernelsize + 1;  
  26.             % 该层需要学习的参数个数。每张特征map是一个(后层特征图数量)*(用来卷积的patch图的大小)  
  27.             % 因为是通过用一个核窗口在上一个特征map层中移动(核窗口每次移动1个像素),遍历上一个特征map  
  28.             % 层的每个神经元。核窗口由kernelsize*kernelsize个元素组成,每个元素是一个独立的权值,所以  
  29.             % 就有kernelsize*kernelsize个需要学习的权值,再加一个偏置值。另外,由于是权值共享,也就是  
  30.             % 说同一个特征map层是用同一个具有相同权值元素的kernelsize*kernelsize的核窗口去感受输入上一  
  31.             % 个特征map层的每个神经元得到的,所以同一个特征map,它的权值是一样的,共享的,权值只取决于  
  32.             % 核窗口。然后,不同的特征map提取输入上一个特征map层不同的特征,所以采用的核窗口不一样,也  
  33.             % 就是权值不一样,所以outputmaps个特征map就有(kernelsize*kernelsize+1)* outputmaps那么多的权值了  
  34.             % 但这里fan_out只保存卷积核的权值W,偏置b在下面独立保存  
  35.             fan_out = net.layers{l}.outputmaps * net.layers{l}.kernelsize ^ 2;  
  36.             for j = 1 : net.layers{l}.outputmaps  %  output map  
  37.                 % fan_out保存的是对于上一层的一张特征map,我在这一层需要对这一张特征map提取outputmaps种特征,  
  38.                 % 提取每种特征用到的卷积核不同,所以fan_out保存的是这一层输出新的特征需要学习的参数个数  
  39.                 % 而,fan_in保存的是,我在这一层,要连接到上一层中所有的特征map,然后用fan_out保存的提取特征  
  40.                 % 的权值来提取他们的特征。也即是对于每一个当前层特征图,有多少个参数链到前层  
  41.                 fan_in = inputmaps * net.layers{l}.kernelsize ^ 2;  
  42.                 for i = 1 : inputmaps  %  input map  
  43.                     % 随机初始化权值,也就是共有outputmaps个卷积核,对上层的每个特征map,都需要用这么多个卷积核  
  44.                     % 去卷积提取特征。  
  45.                     % rand(n)是产生n×n的 0-1之间均匀取值的数值的矩阵,再减去0.5就相当于产生-0.5到0.5之间的随机数  
  46.                     % 再 *2 就放大到 [-1, 1]。然后再乘以后面那一数,why?  
  47.                     % 反正就是将卷积核每个元素初始化为[-sqrt(6 / (fan_in + fan_out)), sqrt(6 / (fan_in + fan_out))]  
  48.                     % 之间的随机数。因为这里是权值共享的,也就是对于一张特征map,所有感受野位置的卷积核都是一样的  
  49.                     % 所以只需要保存的是 inputmaps * outputmaps 个卷积核。  
  50.                     net.layers{l}.k{i}{j} = (rand(net.layers{l}.kernelsize) - 0.5) * 2 * sqrt(6 / (fan_in + fan_out));  
  51.                 end  
  52.                 net.layers{l}.b{j} = 0; % 将偏置初始化为0  
  53.             end  
  54.             % 只有在卷积层的时候才会改变特征map的个数,pooling的时候不会改变个数。这层输出的特征map个数就是  
  55.             % 输入到下一层的特征map个数  
  56.             inputmaps = net.layers{l}.outputmaps;   
  57.         end  
  58.     end  
  59.       
  60.     % fvnum 是输出层的前面一层的神经元个数。  
  61.     % 这一层的上一层是经过pooling后的层,包含有inputmaps个特征map。每个特征map的大小是mapsize。  
  62.     % 所以,该层的神经元个数是 inputmaps * (每个特征map的大小)  
  63.     % prod: Product of elements.  
  64.     % For vectors, prod(X) is the product of the elements of X  
  65.     % 在这里 mapsize = [特征map的行数 特征map的列数],所以prod后就是 特征map的行*列  
  66.     fvnum = prod(mapsize) * inputmaps;  
  67.     % onum 是标签的个数,也就是输出层神经元的个数。你要分多少个类,自然就有多少个输出神经元  
  68.     onum = size(y, 1);  
  69.   
  70.     % 这里是最后一层神经网络的设定  
  71.     % ffb 是输出层每个神经元对应的基biases  
  72.     net.ffb = zeros(onum, 1);  
  73.     % ffW 输出层前一层 与 输出层 连接的权值,这两层之间是全连接的  
  74.     net.ffW = (rand(onum, fvnum) - 0.5) * 2 * sqrt(6 / (onum + fvnum));  
  75. end  


cnntrain.m

[plain]  view plain copy
  1. function net = cnntrain(net, x, y, opts)  
  2.     m = size(x, 3); % m 保存的是 训练样本个数  
  3.     numbatches = m / opts.batchsize;  
  4.     % rem: Remainder after division. rem(x,y) is x - n.*y 相当于求余  
  5.     % rem(numbatches, 1) 就相当于取其小数部分,如果为0,就是整数  
  6.     if rem(numbatches, 1) ~= 0  
  7.         error('numbatches not integer');  
  8.     end  
  9.       
  10.     net.rL = [];  
  11.     for i = 1 : opts.numepochs  
  12.         % disp(X) 打印数组元素。如果X是个字符串,那就打印这个字符串  
  13.         disp(['epoch ' num2str(i) '/' num2str(opts.numepochs)]);  
  14.         % tic 和 toc 是用来计时的,计算这两条语句之间所耗的时间  
  15.         tic;  
  16.         % P = randperm(N) 返回[1, N]之间所有整数的一个随机的序列,例如  
  17.         % randperm(6) 可能会返回 [2 4 5 6 1 3]  
  18.         % 这样就相当于把原来的样本排列打乱,再挑出一些样本来训练  
  19.         kk = randperm(m);  
  20.         for l = 1 : numbatches  
  21.             % 取出打乱顺序后的batchsize个样本和对应的标签  
  22.             batch_x = x(:, :, kk((l - 1) * opts.batchsize + 1 : l * opts.batchsize));  
  23.             batch_y = y(:,    kk((l - 1) * opts.batchsize + 1 : l * opts.batchsize));  
  24.   
  25.             % 在当前的网络权值和网络输入下计算网络的输出  
  26.             net = cnnff(net, batch_x); % Feedforward  
  27.             % 得到上面的网络输出后,通过对应的样本标签用bp算法来得到误差对网络权值  
  28.             %(也就是那些卷积核的元素)的导数  
  29.             net = cnnbp(net, batch_y); % Backpropagation  
  30.             % 得到误差对权值的导数后,就通过权值更新方法去更新权值  
  31.             net = cnnapplygrads(net, opts);  
  32.             if isempty(net.rL)  
  33.                 net.rL(1) = net.L; % 代价函数值,也就是误差值  
  34.             end  
  35.             net.rL(end + 1) = 0.99 * net.rL(end) + 0.01 * net.L; % 保存历史的误差值,以便画图分析  
  36.         end  
  37.         toc;  
  38.     end  
  39.       
  40. end  


cnnff.m

[plain]  view plain copy
  1. function net = cnnff(net, x)  
  2.     n = numel(net.layers); % 层数  
  3.     net.layers{1}.a{1} = x; % 网络的第一层就是输入,但这里的输入包含了多个训练图像  
  4.     inputmaps = 1; % 输入层只有一个特征map,也就是原始的输入图像  
  5.   
  6.     for l = 2 : n   %  for each layer  
  7.         if strcmp(net.layers{l}.type, 'c') % 卷积层  
  8.             %  !!below can probably be handled by insane matrix operations  
  9.             % 对每一个输入map,或者说我们需要用outputmaps个不同的卷积核去卷积图像  
  10.             for j = 1 : net.layers{l}.outputmaps   %  for each output map  
  11.                 %  create temp output map  
  12.                 % 对上一层的每一张特征map,卷积后的特征map的大小就是   
  13.                 % (输入map宽 - 卷积核的宽 + 1)* (输入map高 - 卷积核高 + 1)  
  14.                 % 对于这里的层,因为每层都包含多张特征map,对应的索引保存在每层map的第三维  
  15.                 % 所以,这里的z保存的就是该层中所有的特征map了  
  16.                 z = zeros(size(net.layers{l - 1}.a{1}) - [net.layers{l}.kernelsize - 1 net.layers{l}.kernelsize - 1 0]);  
  17.                 for i = 1 : inputmaps   %  for each input map  
  18.                     %  convolve with corresponding kernel and add to temp output map  
  19.                     % 将上一层的每一个特征map(也就是这层的输入map)与该层的卷积核进行卷积  
  20.                     % 然后将对上一层特征map的所有结果加起来。也就是说,当前层的一张特征map,是  
  21.                     % 用一种卷积核去卷积上一层中所有的特征map,然后所有特征map对应位置的卷积值的和  
  22.                     % 另外,有些论文或者实际应用中,并不是与全部的特征map链接的,有可能只与其中的某几个连接  
  23.                     z = z + convn(net.layers{l - 1}.a{i}, net.layers{l}.k{i}{j}, 'valid');  
  24.                 end  
  25.                 %  add bias, pass through nonlinearity  
  26.                 % 加上对应位置的基b,然后再用sigmoid函数算出特征map中每个位置的激活值,作为该层输出特征map  
  27.                 net.layers{l}.a{j} = sigm(z + net.layers{l}.b{j});  
  28.             end  
  29.             %  set number of input maps to this layers number of outputmaps  
  30.             inputmaps = net.layers{l}.outputmaps;  
  31.         elseif strcmp(net.layers{l}.type, 's') % 下采样层  
  32.             %  downsample  
  33.             for j = 1 : inputmaps  
  34.                 %  !! replace with variable  
  35.                 % 例如我们要在scale=2的域上面执行mean pooling,那么可以卷积大小为2*2,每个元素都是1/4的卷积核  
  36.                 z = convn(net.layers{l - 1}.a{j}, ones(net.layers{l}.scale) / (net.layers{l}.scale ^ 2), 'valid');   
  37.                 % 因为convn函数的默认卷积步长为1,而pooling操作的域是没有重叠的,所以对于上面的卷积结果  
  38.                 % 最终pooling的结果需要从上面得到的卷积结果中以scale=2为步长,跳着把mean pooling的值读出来  
  39.                 net.layers{l}.a{j} = z(1 : net.layers{l}.scale : end, 1 : net.layers{l}.scale : end, :);  
  40.             end  
  41.         end  
  42.     end  
  43.   
  44.     %  concatenate all end layer feature maps into vector  
  45.     % 把最后一层得到的特征map拉成一条向量,作为最终提取到的特征向量  
  46.     net.fv = [];  
  47.     for j = 1 : numel(net.layers{n}.a) % 最后一层的特征map的个数  
  48.         sa = size(net.layers{n}.a{j}); % 第j个特征map的大小  
  49.         % 将所有的特征map拉成一条列向量。还有一维就是对应的样本索引。每个样本一列,每列为对应的特征向量  
  50.         net.fv = [net.fv; reshape(net.layers{n}.a{j}, sa(1) * sa(2), sa(3))];  
  51.     end  
  52.     %  feedforward into output perceptrons  
  53.     % 计算网络的最终输出值。sigmoid(W*X + b),注意是同时计算了batchsize个样本的输出值  
  54.     net.o = sigm(net.ffW * net.fv + repmat(net.ffb, 1, size(net.fv, 2)));  
  55.   
  56. end  


cnnbp.m

[plain]  view plain copy
  1. function net = cnnbp(net, y)  
  2.     n = numel(net.layers); % 网络层数  
  3.   
  4.     %  error  
  5.     net.e = net.o - y;   
  6.     %  loss function  
  7.     % 代价函数是 均方误差  
  8.     net.L = 1/2* sum(net.e(:) .^ 2) / size(net.e, 2);  
  9.   
  10.     %%  backprop deltas  
  11.     % 这里可以参考 UFLDL 的 反向传导算法 的说明  
  12.     % 输出层的 灵敏度 或者 残差  
  13.     net.od = net.e .* (net.o .* (1 - net.o));   %  output delta  
  14.     % 残差 反向传播回 前一层  
  15.     net.fvd = (net.ffW' * net.od);              %  feature vector delta  
  16.     if strcmp(net.layers{n}.type, 'c')         %  only conv layers has sigm function  
  17.         net.fvd = net.fvd .* (net.fv .* (1 - net.fv));  
  18.     end  
  19.   
  20.     %  reshape feature vector deltas into output map style  
  21.     sa = size(net.layers{n}.a{1}); % 最后一层特征map的大小。这里的最后一层都是指输出层的前一层  
  22.     fvnum = sa(1) * sa(2); % 因为是将最后一层特征map拉成一条向量,所以对于一个样本来说,特征维数是这样  
  23.     for j = 1 : numel(net.layers{n}.a) % 最后一层的特征map的个数  
  24.         % 在fvd里面保存的是所有样本的特征向量(在cnnff.m函数中用特征map拉成的),所以这里需要重新  
  25.         % 变换回来特征map的形式。d 保存的是 delta,也就是 灵敏度 或者 残差  
  26.         net.layers{n}.d{j} = reshape(net.fvd(((j - 1) * fvnum + 1) : j * fvnum, :), sa(1), sa(2), sa(3));  
  27.     end  
  28.   
  29.     % 对于 输出层前面的层(与输出层计算残差的方式不同)  
  30.     for l = (n - 1) : -1 : 1  
  31.         if strcmp(net.layers{l}.type, 'c')  
  32.             for j = 1 : numel(net.layers{l}.a) % 该层特征map的个数  
  33.                 % net.layers{l}.d{j} 保存的是 第l层 的 第j个 map 的 灵敏度map。 也就是每个神经元节点的delta的值  
  34.                 % expand的操作相当于对l+1层的灵敏度map进行上采样。然后前面的操作相当于对该层的输入a进行sigmoid求导  
  35.                 % 这条公式请参考 Notes on Convolutional Neural Networks  
  36.                 % for k = 1:size(net.layers{l + 1}.d{j}, 3)  
  37.                     % net.layers{l}.d{j}(:,:,k) = net.layers{l}.a{j}(:,:,k) .* (1 - net.layers{l}.a{j}(:,:,k)) .*  kron(net.layers{l + 1}.d{j}(:,:,k), ones(net.layers{l + 1}.scale)) / net.layers{l + 1}.scale ^ 2;  
  38.                 % end  
  39.                 net.layers{l}.d{j} = net.layers{l}.a{j} .* (1 - net.layers{l}.a{j}) .* (expand(net.layers{l + 1}.d{j}, [net.layers{l + 1}.scale net.layers{l + 1}.scale 1]) / net.layers{l + 1}.scale ^ 2);  
  40.             end  
  41.         elseif strcmp(net.layers{l}.type, 's')  
  42.             for i = 1 : numel(net.layers{l}.a) % 第l层特征map的个数  
  43.                 z = zeros(size(net.layers{l}.a{1}));  
  44.                 for j = 1 : numel(net.layers{l + 1}.a) % 第l+1层特征map的个数  
  45.                      z = z + convn(net.layers{l + 1}.d{j}, rot180(net.layers{l + 1}.k{i}{j}), 'full');  
  46.                 end  
  47.                 net.layers{l}.d{i} = z;  
  48.             end  
  49.         end  
  50.     end  
  51.   
  52.     %%  calc gradients  
  53.     % 这里与 Notes on Convolutional Neural Networks 中不同,这里的 子采样 层没有参数,也没有  
  54.     % 激活函数,所以在子采样层是没有需要求解的参数的  
  55.     for l = 2 : n  
  56.         if strcmp(net.layers{l}.type, 'c')  
  57.             for j = 1 : numel(net.layers{l}.a)  
  58.                 for i = 1 : numel(net.layers{l - 1}.a)  
  59.                     % dk 保存的是 误差对卷积核 的导数  
  60.                     net.layers{l}.dk{i}{j} = convn(flipall(net.layers{l - 1}.a{i}), net.layers{l}.d{j}, 'valid') / size(net.layers{l}.d{j}, 3);  
  61.                 end  
  62.                 % db 保存的是 误差对于bias基 的导数  
  63.                 net.layers{l}.db{j} = sum(net.layers{l}.d{j}(:)) / size(net.layers{l}.d{j}, 3);  
  64.             end  
  65.         end  
  66.     end  
  67.     % 最后一层perceptron的gradient的计算  
  68.     net.dffW = net.od * (net.fv)' / size(net.od, 2);  
  69.     net.dffb = mean(net.od, 2);  
  70.   
  71.     function X = rot180(X)  
  72.         X = flipdim(flipdim(X, 1), 2);  
  73.     end  
  74. end  


cnnapplygrads.m

[plain]  view plain copy
  1. function net = cnnapplygrads(net, opts)  
  2.     for l = 2 : numel(net.layers)  
  3.         if strcmp(net.layers{l}.type, 'c')  
  4.             for j = 1 : numel(net.layers{l}.a)  
  5.                 for ii = 1 : numel(net.layers{l - 1}.a)  
  6.                     % 这里没什么好说的,就是普通的权值更新的公式:W_new = W_old - alpha * de/dW(误差对权值导数)  
  7.                     net.layers{l}.k{ii}{j} = net.layers{l}.k{ii}{j} - opts.alpha * net.layers{l}.dk{ii}{j};  
  8.                 end  
  9.             end  
  10.             net.layers{l}.b{j} = net.layers{l}.b{j} - opts.alpha * net.layers{l}.db{j};  
  11.         end  
  12.     end  
  13.   
  14.     net.ffW = net.ffW - opts.alpha * net.dffW;  
  15.     net.ffb = net.ffb - opts.alpha * net.dffb;  
  16. end  


cnntest.m

[plain]  view plain copy
  1. function [er, bad] = cnntest(net, x, y)  
  2.     %  feedforward  
  3.     net = cnnff(net, x); % 前向传播得到输出  
  4.     % [Y,I] = max(X) returns the indices of the maximum values in vector I  
  5.     [~, h] = max(net.o); % 找到最大的输出对应的标签  
  6.     [~, a] = max(y);     % 找到最大的期望输出对应的索引  
  7.     bad = find(h ~= a);  % 找到他们不相同的个数,也就是错误的次数  
  8.   
  9.     er = numel(bad) / size(y, 2); % 计算错误率  
  10. end  
深度学习之卷积神经网络CNN做手写体识别的VS代码。支持linux版本和VS2012版本。 tiny-cnn: A C++11 implementation of convolutional neural networks ======== tiny-cnn is a C++11 implementation of convolutional neural networks. design principle ----- * fast, without GPU 98.8% accuracy on MNIST in 13 minutes training (@Core i7-3520M) * header only, policy-based design supported networks ----- ### layer-types * fully-connected layer * convolutional layer * average pooling layer ### activation functions * tanh * sigmoid * rectified linear * identity ### loss functions * cross-entropy * mean-squared-error ### optimization algorithm * stochastic gradient descent (with/without L2 normalization) * stochastic gradient levenberg marquardt dependencies ----- * boost C++ library * Intel TBB sample code ------ ```cpp #include "tiny_cnn.h" using namespace tiny_cnn; // specify loss-function and optimization-algorithm typedef network CNN; // tanh, 32x32 input, 5x5 window, 1-6 feature-maps convolution convolutional_layer C1(32, 32, 5, 1, 6); // tanh, 28x28 input, 6 feature-maps, 2x2 subsampling average_pooling_layer S2(28, 28, 6, 2); // fully-connected layers fully_connected_layer F3(14*14*6, 120); fully_connected_layer F4(120, 10); // connect all CNN mynet; mynet.add(&C1); mynet.add(&S2); mynet.add(&F3); mynet.add(&F4); assert(mynet.in_dim() == 32*32); assert(mynet.out_dim() == 10); ``` more sample, read main.cpp build sample program ------ ### gcc(4.6~) without tbb ./waf configure --BOOST_ROOT=your-boost-root ./waf build with tbb ./waf configure --TBB --TBB_ROOT=your-tbb-root --BOOST_ROOT=your-boost-root ./waf build with tbb and SSE/AVX ./waf configure --AVX --TBB --TBB_ROOT=your-tbb-root --BOOST_ROOT=your-boost-root ./waf build ./waf configure --SSE --TBB --TBB_ROOT=your-tbb-root --BOOST_ROOT=your-boost-root ./waf build or edit inlude/config.h to customize default behavior. ### vc(2012~) open vc/tiny_cnn.sln and build in release mode.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值