使用Pytorch识别字符验证码

之前已经学习过利用Keras搭建神经网络模型来识别字符验证码,相关的文章:
字符验证码识别之数据预处理
涉及图像预处理和标签处理等操作

字符验证码识别之模型构建
涉及模型构建以及训练过程。

近期又学习了pytorch实现卷积神经网络相关的技术,正好遇到一个验证码识别的需求,所以尝试使用pytorch来实现。

数据预处理

要训练的验证码如下所示:

其为中文汉字的简单运算,实际上仅包括零壹贰叁肆伍陆柒捌玖加减乘等于,这15个汉字,等于可以不识别(其实识别也完全没问题,只不过问题能简化就尽量简化嘛),那最后也就是总共要识别13个汉字,分类数就是13。

另外我们可以用’0123456789±x’来代替汉字,避免文件名称无法使用汉字(windows下open-cv不能读取带有中文路径或文件名称)的问题。

下载并标注了1000张验证码,观察其字体颜色和干扰线、点均多变,无法根据特定规则将其区分;另外,尝试中值模糊、均值模糊和高斯模糊,均得不到较好的效果(肉眼观察)。只有灰度化和二值化后,感觉稍微变得清晰了一些。

另外,针对数据集我还统计了一下各个类别的数量是否均衡(主要怕有的文字训练样本太少,训练效果差)。

{'捌': 206, '减': 346, '肆': 220, '柒': 205, '零': 200, '伍': 214, '加': 358,
'玖': 189, '壹': 195, '叁': 191, '陆': 206, '乘': 297, '贰': 176}

数据集的分布情况如上数据,总体还算均衡,那就表示可以开始处理数据和进行训练了。

在进一步处理数据之前,先划分数据集,800个训练集、验证集和测试集分别100,划分完数据集后,我也是统计了下各个数据集下的分类数量是否均衡(毕竟以前犯过划分数据集有问题的错误)。

以上都是一些简单的操作,下面仅展示后续将图片和标签转换成numpy矩阵的代码:

import os
import cv2
import numpy as np
import random
from os import remove
import mathclass ImageProcess:channel = 1height = 40width = 90num_classes = 13  # 共13个汉字labels_len = 3  # 每个标签包含3个汉字words = '0123456789+-x'  # 用字符来代替汉字images_path_train = 'D:/captcha/shanghai/train/'images_path_val = 'D:/captcha/shanghai/val/'images_path_test = 'D:/captcha/shanghai/test/'images_train = os.listdir(images_path_train)images_val = os.listdir(images_path_val)images_test = os.listdir(images_path_test)def __init__(self):self.x_data_train = Noneself.y_data_train = Noneself.x_data_val = Noneself.y_data_val = Noneself.x_data_test = Noneself.y_data_test = Noneprint('预处理图像...')self.process_image("train")self.process_image("test")self.process_image("val")print('预处理标签')self.process_label("train")self.process_label("test")self.process_label("val")print('处理完成')def process_label(self, which):"""处理标签如果每个样本是单类别,每个类别就一个值,处理成一个长度为batch的列表就可以如果每个样本是多类别(假设为n, n>=2),处理成[batch, n]的二维数组:param: which 处理哪个数据集:return:"""labels_list = []if which == "train":images = self.images_trainelif which == "test":images = self.images_testelse:images = self.images_valfor image in images:labels = image.split("_")[1].replace('.jpg', '')"""这部分是ont-hot编码的处理逻辑,在pytorch种实际不需要这样处理,这主要取决于 nn.CrossEntropyLoss()的输入参数格式参数只需要标签即可, 不需要传one-hot向量"""# 初始化一个 3x13 的矩阵,初始值为0.0# result = np.zeros((self.labels_len, self.num_classes), dtype='float32')# for i, c in enumerate(labels):#     result[i][self.words.index(c)] = 1"""直接处理为 [batch, n]的二维数组 即可"""result = []for label in labels:result.append(self.words.index(label))labels_list.append(result)if which == "train":self.y_data_train = np.array(labels_list, dtype='int32')elif which == "test":self.y_data_test = np.array(labels_list, dtype='int32')else:self.y_data_val = np.array(labels_list, dtype='int32')def process_image(self, which):"""处理图片 处理目标 (batch, channel, height, width):return:"""images_list = []if which == "train":images = self.images_trainimages_path = self.images_path_trainelif which == "test":images = self.images_testimages_path = self.images_path_testelse:images = self.images_valimages_path = self.images_path_valfor image in images:path = f'{images_path}{image}'img = cv2.imread(path)# 中值模糊# img = cv2.medianBlur(img, 3)# 均值模糊# img = cv2.blur(img, (2, 2))# 高斯模糊# img = cv2.GaussianBlur(img, (5, 5), 1)# 灰度化img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)# 二值化ret, img = cv2.threshold(img, 0, 255, cv2.THRESH_OTSU)# cv2.namedWindow('captcha', cv2.WINDOW_NORMAL | cv2.WINDOW_KEEPRATIO)# cv2.resizeWindow('captcha', 180, 80)# cv2.imshow('captcha', img)# cv2.waitKey(0)img = np.array(img, dtype='float32')# 归一化img /= 255images_list.append(np.reshape(img, (1, self.height, self.width)))if which == "train":self.x_data_train = np.array(images_list, dtype='float32')elif which == "test":self.x_data_test = np.array(images_list, dtype='float32')else:self.x_data_val = np.array(images_list, dtype='float32')def train_loader(self, batch_size=16):"""按批次,将训练数据和标签 迭代返回:param batch_size::return:"""batch_nums = math.ceil(len(self.x_data_train)/batch_size)for i in range(batch_nums):x_train = self.x_data_train[i*batch_size:(i+1)*batch_size]y_train = self.y_data_train[i*batch_size:(i+1)*batch_size]yield x_train, y_traindef test_loader(self, batch_size=16):"""按批次,将测试数据和标签 迭代返回:param batch_size::return:"""batch_nums = math.ceil(len(self.x_data_test)/batch_size)for i in range(batch_nums):x_test = self.x_data_test[i*batch_size:(i+1)*batch_size]y_test = self.y_data_test[i*batch_size:(i+1)*batch_size]yield x_test, y_testdef val_loader(self, batch_size=16):"""按批次,将验证数据和标签 迭代返回:param batch_size::return:"""batch_nums = math.ceil(len(self.x_data_val)/batch_size)for i in range(batch_nums):x_val = self.x_data_val[i*batch_size:(i+1)*batch_size]y_val = self.y_data_val[i*batch_size:(i+1)*batch_size]yield x_val, y_val

关于代码核心的地方,在代码中都有注释。

另外需要注意的一点是,如果输入到神经网络中的图片为三维,则

images_list.append(np.reshape(rr_img, (1, self.height, self.width)))

要替换为

images_list.append(np.transpose(img, (2, 0, 1)))

否则reshape会导致整个数据错乱。

搭建模型

之前使用Keras做字符验证码识别的时候,得到的经验就是针对这种比较简单的字符验证码,无需过于复杂的模型,几层CNN就够了。

import torch
from torch import nn
from torch import optimimport osclass NeuralNetWork(nn.Module):def __init__(self, channel, num_classes):""":param channel: 输入图片的channel:param num_classes: 分类数量"""super(NeuralNetWork, self).__init__()self.convin = nn.Sequential(nn.Conv2d(channel, 64, kernel_size=(3, 3), padding=1, bias=False),nn.ReLU(),nn.Conv2d(64, 64, kernel_size=(3, 3), padding=1, bias=False),nn.ReLU(),nn.MaxPool2d(2, 2),nn.Dropout(0.25))self.convall = nn.Sequential(nn.Conv2d(64, 64, kernel_size=(3, 3), padding=1, bias=False),nn.ReLU(),nn.Conv2d(64, 64, kernel_size=(3, 3), padding=1, bias=False),nn.ReLU(),nn.MaxPool2d(2, 2),nn.Dropout(0.25))# 承接卷积层和fc层self.fc1 = nn.Sequential(nn.Linear(64*5*11, 1024),  # 这个输入值需要计算,根据输入图像的尺寸决定(本次输入图像尺寸为40*90)nn.ReLU(),nn.Dropout(0.5))self.dense1 = nn.Sequential(nn.Linear(1024, 512),nn.ReLU(),nn.Linear(512, num_classes),# nn.LogSoftmax())self.dense2 = nn.Sequential(nn.Linear(1024, 512),nn.ReLU(),nn.Linear(512, num_classes),# nn.LogSoftmax())self.dense3 = nn.Sequential(nn.Linear(1024, 512),nn.ReLU(),nn.Linear(512, num_classes),# nn.LogSoftmax())def forward(self, n_input):# 进行卷积、激活和池化操作feature = self.convin(n_input)feature = self.convall(feature)feature = self.convall(feature)# 对特征层(Tensor类型)进行维度变换,变成两维feature = feature.view(n_input.size(0), -1)  # size(0)是批次大小# 进行全连接操作feature = self.fc1(feature)out_put1 = self.dense1(feature)out_put2 = self.dense2(feature)out_put3 = self.dense3(feature)# 每个样本有三个输出值return [out_put1, out_put2, out_put3]

关于模型代码,有以下几点说明:

  1. 使用几层卷积、卷积核的数量、池化操作和dropout等并不是固定的,这要根据你的训练情况逐步调整;

  2. 全连接层的地方的输入值是需要计算的,是由输入到全连接层的输出通道数量x你的图片经过你的卷积和池化层后得到的尺寸,比如这里输出通道数量为64,原始输入图片尺寸为40x90,经过padding=1的卷积层尺寸不变,经过三次(2, 2)的池化层,变为5x11

    40x90 --> 20x45 --> 10x22 --> 5x11

    并且在全连接层之前要把feature转换为(batch, )形状的二维tensor。

  3. 如何控制每个样本有3个输出值,这里是我遇到的难题,因为之前学习都是每个样本一个类型。

    这里经过咨询有经验的同事得知,实际上就是利用相同的线性层计算得到三个值,同时返回。

    不过需要注意的是,即使这三个输出值是经过了相同的线性层,就像这里的

    nn.Sequential(nn.Linear(1024, 512),nn.ReLU(),nn.Linear(512, num_classes),
    )
    

    但是一定是三个独立定义的层(层名称无所谓),如果均使用同一个层,那么输出的这三个值永远都是一样的(亲身踩坑)

编写训练代码

from image_process import ImageProcessif __name__ == "__main__":net = NeuralNetWork(1, 13)  # channel=1,classes=13epochs = 100  # 设置训练轮次batch_size = 16# 训练部分代码criterion = nn.CrossEntropyLoss()  # 交叉熵损失函数# 随机梯度下降优化# optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)optimizer = optim.Adam(net.parameters(), lr=0.001, weight_decay=1e-6)ip = ImageProcess()val_loss_min = 0  # 保存训练过程中的最小损失(验证)for epoch in range(epochs):net.train()  # 训练与测试,BN和Dropout有区别# 如果没有BN和Dropout,或者只训练不验证,可以不执行该方法train_loss = 0.0  # 实时打印当前损失变化情况for batch_idx, data in enumerate(ip.train_loader(batch_size=batch_size)):inputs, labels = datainputs = torch.from_numpy(inputs)  # 从numpy array转成tensorlabels = torch.from_numpy(labels).long()  # 输入损失函数要求type为longoptimizer.zero_grad()  # 先将梯度设置为0out_puts = net(inputs)  # 前向传播# out_puts的shape(n, batch_size, num_classes) 3x16x13 n表示每个样本包含的分类数量# 这里因为输出多个值,所以计算损失把多个损失加在一起# labels的shape(batch_size, n) 16x3loss = (criterion(out_puts[0], labels[:, 0]) +criterion(out_puts[1], labels[:, 1]) +criterion(out_puts[2], labels[:, 2]))loss.backward()  # 反向传播optimizer.step()# 查看网络训练状态(损失是计算几批数据的平均损失)train_loss += loss.item()# 800个训练样本,batch_size=16, 800/16 = 50(一共50批次)# 每10批,打印一次损失if (batch_idx+1) % 10 == 0:print(f'epoch: {epoch+1}, batch_inx: {batch_idx+1} train loss: {train_loss/160}')train_loss = 0.0state = {'net': net.state_dict(),'epoch': epoch+1}if not os.path.isdir('checkpoint'):os.mkdir('checkpoint')if (epoch+1) % 10 == 0:   # 每10轮保存一次权重print(f'saving epoch {epoch+1} mode ...')torch.save(state, f'./checkpoint/shanghai_epoch_{epoch+1}.pth')  # pth 与 ckpt# 验证部分net.eval()val_loss = 0.0for batch_idx, val_data in enumerate(ip.val_loader(16)):inputs, labels = val_datainputs = torch.from_numpy(inputs)  # 从numpy array转成tensorlabels = torch.from_numpy(labels).long()  # 输入损失函数要求type为longout_puts = net(inputs)loss = (criterion(out_puts[0], labels[:, 0]) +criterion(out_puts[1], labels[:, 1]) +criterion(out_puts[2], labels[:, 2]))val_loss += loss.item()# 100个训练样本,batch_size=16, 100/16 = 6(一共7批次)# 一轮计算一次平均损失if (batch_idx+1) % 7 == 0:print(f'epoch: {epoch+1}, batch_inx: {batch_idx+1} val loss: {val_loss/100}')if not val_loss_min:val_loss_min = val_loss# 正常是每10轮保存一次权重,当发现这一轮验证损失更小时,也会保存一次权重elif val_loss_min >= val_loss:val_loss_min = val_lossprint(f'saving epoch {epoch+1} mode ...')torch.save(state, f'./checkpoint/shanghai_epoch_{epoch+1}.pth')val_loss = 0.0print('training task finished')

关于训练代码,有以下几点说明:

  1. 这里批量加载训练集和验证集是我在前面数据预处理部分特别开发好的,我觉得还是蛮巧妙地;

  2. pytorch中都是使用tensor,所以需要将加载的数据(numpy矩阵)转换成tensor:

    torch.from_numpy(inputs)

  3. 最重要的一点是三个输出的情况下,如何计算损失,这是我开发过程中遇到的另一个难题。

    经过咨询有经验的同事得知,实际上就是将三个输出的损失加在一起,但是你要根据神经网络的数据输出格式和你自己的标签格式,将正确的数据输入到损失函数中进行计算,且要注意CrossEntropyLoss的输入参数格式

    关于CrossEntropyLoss使用方式的介绍:

    import torch
    from torch import nnx = torch.tensor([[0.2, 0.3, 0.5, 0.1], [0.3, 0.01, 0.02, 0.4]])
    y = torch.tensor([2, 3])
    criterion = nn.CrossEntropyLoss()
    loss = criterion(x, y)
    print(loss)
    

    其y参数只需要标签即可, 不需要传one-hot向量,这也就是前面数据预处理时没有采用one-hot编码来处理标签的原因。另外out_puts的输出shape我在代码中也有注释。

记录一些训练过程中遇到的情况

  1. 训练到第30轮,训练损失才开始明显下降,一度让我以为程序哪里有问题,经过上网查资料发现:损失函数(loss)在最初的几个epochs时没有下降,可能的原因是学习率设置的太低、正则参数太高和陷入局部最小值。

    我当时设置的学习率lr=0.0001,确实比较小,我尝试调整为lr=0.001再训练,发现在第20轮时损失就开始下降了,果然是学习率设置的太低。

    另外我觉得dropout的太多有可能也是导致损失延迟降低的原因,所以我尝试将dropout的值缩小,也能提前几轮损失开始下降,但是最后的训练效果却不如dropout较大的时候。

  2. 在较前面的轮次,val_loss远小于train_loss,一开始我总结的原因是在网络中添加了dropout层,而dropout仅在训练时生效,测试时是不会dropout的。所以val_loss会小于train_loss,因为我这里是远小于,后经排查是计算的时候写了bug。

    net.train()  # 训练与测试,BN和Dropout有区别net.eval() # 验证部分
    

    也就是这两行代码的作用,执行后告诉神经网络接下来将进入训练模式还是测试模型;另外BN层也是仅在训练时生效,在测试时不使用。

  3. 关于如何设计出较好的模型,目前是我能力欠缺的一个地方,上面代码使用的模型架构(经过80轮的训练准确率能达到80%,经过250轮的训练准确率能达到90%),是参考大佬的模型,而我自己设计的模型准确率最高仅能达到70%,并且我也经过多次调整和训练,效果也并没有显著提升。

测试

import torchfrom train import NeuralNetWork
from image_process import ImageProcessif __name__ == "__main__":# 测试net = NeuralNetWork(1, 13)# 如果有dropout和BN操作,这里一定执行该方法,表示网络接下来进行测试操作net.eval() check_point = torch.load('./checkpoint/shanghai_epoch_27.pth')# check_point = torch.load('shanghai_epoch_80.pth')net.load_state_dict(check_point['net'])batch_size = 16ip = ImageProcess()total_image = 0  # 总的图片数量correct_image = 0total_label = 0  # 总的标签数量correct_label = 0for data in ip.test_loader(batch_size):images, labels = dataimages = torch.from_numpy(images)out_puts = net(images)# batch_result = []_, predicted1 = torch.max(out_puts[0], 1)_, predicted2 = torch.max(out_puts[1], 1)_, predicted3 = torch.max(out_puts[2], 1)# batch_result.append(temp_result)for i in range(labels.shape[0]):total_image += 1total_label += 3print(f'true label: {labels[i]}')true_label = labels[i]print(f'predicted label: {predicted1[i]}  {predicted2[i]}  {predicted3[i]}')predicted_label = [int(predicted1[i]), int(predicted2[i]), int(predicted3[i])]if list(true_label) == predicted_label:correct_image += 1if true_label[0] == predicted_label[0]:correct_label += 1if true_label[1] == predicted_label[1]:correct_label += 1if true_label[2] == predicted_label[2]:correct_label += 1print(f'correct_image / total_image: {correct_image}/{total_image}')print(f'correct_label / total_label: {correct_label}/{total_label}')

这里的测试是批量测试,与实际的预测方法还有区别,但是大同小异,只不过在预测的方法中要注意针对单张图片再增加一个维度表示批次,否则传入神经网络的数据格式会出问题。

尝试进一步优化

  1. 添加BN

    self.convin = nn.Sequential(nn.Conv2d(channel, 64, kernel_size=(3, 3), padding=1, bias=False),nn.BatchNorm2d(64),nn.ReLU(),nn.Conv2d(64, 64, kernel_size=(3, 3), padding=1, bias=False),nn.BatchNorm2d(64),nn.ReLU(),nn.MaxPool2d(2, 2),nn.Dropout(0.25)
    )

    添加批规范化层后,训练得到的模型效果并没有不添加之前好,不过训练损失则在第2~3轮就开始明显下降了,不使用BN层的话,要训练15~20轮,训练损失才开始明显下降。

    考虑到BN层的目标就是防止梯度消失或爆炸、加快训练速度,所以损失下降比较快就体现了BN层的用处,但是针对我这个项目,整体效果却并没有提升。

  2. 旋转图片

    因为观察验证码会稍微有些倾斜,倾斜幅度很小,所以想着能不能利用数据增强(旋转一个很小的角度)来进一步提升准确率。

​ 为了进行数据增强,我是直接在process_image方法中,对每一张图片进行旋转,然后生成一张新的直接添加 到训练集中,另外标签也要添加两遍,这样我的训练集就变成了1600张,这种方法有个缺点就是一张图片和他的旋转图是挨着的两个样本,在训练时如果可以彻底打乱比较好,而且不知道是不是这个原因,再次训练时,损失吃吃降不下来了(到60轮没有下降,我就停了),但是添加BN层后快速下降(但最终效果没有提升)。

​ 这里记录下旋转用到的技术:

  from torchvision.transforms import transforms# 随机旋转图像def random_rotation(image):image = Image.fromarray(image)  # 传入的image为CV2对象,转换为PIL.Image格式# image.show()rr = transforms.RandomRotation(degrees=(5, 10))rr_image = rr(image)# rr_image.show()return rr_image  # 返回的依然是PIL.Image格式,但是同样可以直接转为np.array

后来我觉得进行数据增强实际上可以对训练集操作然后直接生成相应的图片保存下来,然后在读取的时候打乱数据集比较方便,感兴趣的可以自己尝试。

我的gtihub博客地址:https://forchenxi.github.io/

另外,如果对投资理财感兴趣的同学,可以关注我的微信公众号:运气与实力。

使用Pytorch识别字符验证码相关推荐

  1. [验证码识别技术]字符验证码杀手--CNN

    字符验证码杀手--CNN 1 abstract 目前随着深度学习,越来越蓬勃的发展,在图像识别和语音识别中也表现出了强大的生产力.对于普通的深度学习爱好者来说,一上来就去跑那边公开的大型数据库,比如I ...

  2. 字符验证码识别项目记录

    2019独角兽企业重金招聘Python工程师标准>>> 项目简介: 最近在做一个有趣的项目,需要对某网站的验证码进行识别. 某网站验证码如图:,像素大小:30x106x3 通过人工标 ...

  3. python神经网络库识别验证码_基于TensorFlow 使用卷积神经网络识别字符型图片验证码...

    本项目使用卷积神经网络识别字符型图片验证码,其基于TensorFlow 框架.它封装了非常通用的校验.训练.验证.识别和调用 API,极大地减低了识别字符型验证码花费的时间和精力. 项目地址:http ...

  4. TensorFlow练习20: 使用深度学习破解字符验证码

    验证码是根据随机字符生成一幅图片,然后在图片中加入干扰象素,用户必须手动填入,防止有人利用机器人自动批量注册.灌水.发垃圾广告等等 . 验证码的作用是验证用户是真人还是机器人:设计理念是对人友好,对机 ...

  5. Ocr技术 识别高级验证码

    光学字符识别(英语:Optical Character Recognition, OCR)是指对文本资料的图像文件进行分析处理,获取文字及版面信息的过程. OCR的概念是在1929年由德国科学家Tau ...

  6. python selenium 验证码识别_Python网络爬虫之如何用代码识别图片验证码

    验证码 当我们在爬取某些网站的时候,对于一些频繁请求,网站会识别你是机器还是人.如果是机器,直接不允许你访问这个网站了,直接返回404或者禁止访问. 最常见的方式就是验证码.验证码的主要功能就是区分当 ...

  7. python 识别图形验证码_Python验证码识别

    大致介绍 在python爬虫爬取某些网站的验证码的时候可能会遇到验证码识别的问题,现在的验证码大多分为四类: 1.计算验证码 2.滑块验证码 3.识图验证码 4.语音验证码 这篇博客主要写的就是识图验 ...

  8. python识别复杂验证码2020_Python识别验证码!学会这步,百分之60的网站你基本都能识别了!...

    识别原理 我们采取一种有监督式学习的方法来识别验证码,包含以下几个步骤 图片处理 - 对图片进行降噪.二值化处理 切割图片 - 将图片切割成单个字符并保存 人工标注 - 对切割的字符图片进行人工标注, ...

  9. python代码标识码_代码分享:使用Python和Tesseract来识别图形验证码

    原标题:代码分享:使用Python和Tesseract来识别图形验证码 *本文原创作者:ipenox,本文属FreeBuf原创奖励计划,未经许可禁止转载 各位在企业中做Web漏洞扫描或者渗透测试的朋友 ...

  10. python 识别图形验证码_Python图片验证码降噪处理实例!此乃识别验证码神技!...

    图片验证码算是网络数据采集上的一道拦路虎,虽然有诸多公开的ORC接口.云打码平台,一旦大规模应用起来,还是内部写程序进行识别处理比较好. 而自己写代码进行识别的话,又有很多种方案,比如最近火热的神经网 ...

最新文章

  1. 未老先呆,这锅熬夜真的要背:生物钟影响阿尔茨海默症的机制被发现
  2. bat批处理重命名问题
  3. PHP 开发环境和组织管理
  4. mybatis plus使用雪花算法_MyBatis-Plus进阶
  5. php mysql_query预处理,php+mysqli使用预处理技术进行数据库查询的方法
  6. C#算法设计排序篇之09-基数排序(附带动画演示程序)
  7. 《运营之光》-- 学习笔记(四)
  8. mysql数据库教程官网_数据库MySQL官方推荐教程-MySQL入门到删库
  9. Mysql之数据库与sql
  10. Roller 博客系统的搭建过程
  11. ad20/ad21/ad22学习笔记(基本包含一套流程)Altium Designer
  12. 消息队列的介绍及配置
  13. Java抓包分析一(基于jnetpcap进行抓包)——抓包环境搭建,获取网卡
  14. Android流媒体播放器
  15. Joplin实现样式更改
  16. 芋头怎么蒸好吃 蒸芋头的技巧有哪些
  17. win10任务栏图标空白透明问题解决
  18. 六西格玛dfss_六西格玛设计(DFSS)的概念、核心及优势
  19. 【腾讯Bugly干货分享】腾讯验证码的十二年
  20. mysql和mongo+查询效率_Mongodb VS Mysql 查询性能

热门文章

  1. 程序物语(八):我心戚戚
  2. 在 MacOS 上使用 Qt 开发 Android APP
  3. 基于RNN实现搜狐新闻数据文本分类
  4. 【只推荐一位】木东居士,数据挖掘的大神!
  5. sqlserver Month()函数取日期不足两位的加 0(转载)
  6. 分享md5解密站源代码,简单的代码就可以实现md5解密
  7. alot英文怎么读_alot of是什么意思
  8. js柯里化的认识(本文转载自https://www.zhangxinxu.com/wordpress/2013/02/js-currying),觉得很有用就记下了
  9. 动态规划入门及规则分析(典型)
  10. Hello Qt——Qt自定义标题栏