{"id":18021213,"url":"https://github.com/ice-tong/pytorch-captcha","last_synced_at":"2025-03-26T22:30:38.978Z","repository":{"id":50331545,"uuid":"177255677","full_name":"ice-tong/pytorch-captcha","owner":"ice-tong","description":"使用pytorch搭建cnn识别验证码","archived":false,"fork":false,"pushed_at":"2020-03-14T08:24:47.000Z","size":752,"stargazers_count":106,"open_issues_count":0,"forks_count":32,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-03-22T14:22:23.882Z","etag":null,"topics":["captcha-breaking","pytorch","pytorch-cnn","pytorch-tutorial"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ice-tong.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-03-23T06:58:44.000Z","updated_at":"2025-03-17T15:47:46.000Z","dependencies_parsed_at":"2022-08-31T21:25:47.453Z","dependency_job_id":null,"html_url":"https://github.com/ice-tong/pytorch-captcha","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ice-tong%2Fpytorch-captcha","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ice-tong%2Fpytorch-captcha/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ice-tong%2Fpytorch-captcha/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ice-tong%2Fpytorch-captcha/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ice-tong","download_url":"https://codeload.github.com/ice-tong/pytorch-captcha/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245747407,"owners_count":20665784,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["captcha-breaking","pytorch","pytorch-cnn","pytorch-tutorial"],"created_at":"2024-10-30T06:09:08.181Z","updated_at":"2025-03-26T22:30:38.622Z","avatar_url":"https://github.com/ice-tong.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# pytorch-captcha\n使用pytorch搭建cnn识别验证码\n\n数据集：\n链接: https://pan.baidu.com/s/1pHSl-5nHJWazXVqda-2IcA 提取码: mv3u 复制这段内容后打开百度网盘手机App，操作更方便哦\n\n下载数据集后在根目录解压，运行train.py开始训练\n\n----------\n\n# pytorch入门实战之验证码识别\n\n\n本文将使用pytorch框架训练一个四层卷积神经网络，用以识别四位数字字母区分大小的验证码。\n\n## 1. 引言 ##\n\n去年四六级查分时候我把准考证号忘了，准考证一时也找不到，最后是靠试准考证号试出来的，因为和我同一个考场的同学准考证号只有最后两位座位号不一样，一个考场不超过30人，遍历座位号就能试出来。\n  \n四六级查分系统有一个四位数字字母验证码，如果能够自动识别验证码，就能不断遍历准考证号查分了，不用手段输入验证码查分，效率大大提高，不知道淘宝上“忘记准考证号帮查四六级分”服务是不是这样做的。\n\n## 2. 数据收集 ##\n\n四六级查分网页链接为[http://cet.neea.edu.cn/cet](http://cet.neea.edu.cn/cet)  \n![四六级查分系统](./pics/cet.png)\n\n首先按Fn+F12使用网页开发者工具抓包看一下验证码是如何请求，以及如何提交查询信息并返回结果。最好不要一次性把三条信息都输对，那样会直接跳到查询结果页，不方便查看提交查询的请求。\n\n![查询的请求](./pics/query.png)\n\n可以很容易的找到提交请求的是一个post请求，请求地址为`http://cache.neea.edu.cn/cet/query`，请求参数有两个，分别是`data`和`v`，`data`是由一串固定字符和准考证号以及姓名组成，`v`则是验证码。通过构建查询请求，我们可以知道验证码是否输入正确。点击获取验证码按钮，可以抓包获取到验证码的请求，将验证码请求以及提交查询写成python代码如下：\n\n\n\tdef get_captcha_img():\n    \tik = '123456789123456'\n    \trand = random.random()\n    \timg_path = '{}/{}.png'.format(false_dir, rand)\n    \timgs_url = 'http://cache.neea.edu.cn/Imgs.do?c=CET\u0026ik={}\u0026t={}'.format(ik, \n    \t                                                      rand)\n    \theaders = {'Referer': 'http://cet.neea.edu.cn/cet'}\n    \tresp = sess.get(imgs_url, headers=headers)\n    \timg_url = re.findall(r'\"([^\"]*)?\"', resp.text)[0]\n    \timg_resp = sess.get(img_url, headers=headers)\n    \twith open(img_path, 'wb') as f:\n    \t    f.write(img_resp.content)\n    \treturn img_path  \n\n\tdef check_captcha(v):\n    \tquery_url = 'http://cache.neea.edu.cn/cet/query'\n    \tdata = {'data': 'CET4_181_DANGCI,123456789123456,萧炎',\n    \t        'v': v}\n    \theaders = {'Referer': 'http://cet.neea.edu.cn/cet'}\n    \tresp = sess.post(query_url, headers=headers, data=data)\n\t\t#    print(resp.text)\n    \tif '抱歉，验证码错误！' in resp.text:\n        \treturn False\n    \telse:\n        \treturn True\n\n结合以上请求验证码以及提交查询信息判断验证码是否正确的方法，再通过打码平台，可以获得带有正确标记的验证码图片。\n使用上述方法，我获得了1181张带有标注的验证码，宽和高为（180，100），将其分为训练集与测试集，训练集为800张，测试及381张。我看过的其他使用卷积神经网络识别验证码的文章，使用的训练集数量多达几千上万张，大多都是自己用程序生成的，本文使用打码平台标记的验证码，就不要求那么大的数据集了，但也能达到满意的效果。\n\n还值得一提的是，使用打码平台标注验证码，成功标注了1181张外，还有将近四百张验证码识别失败，粗略估计，这个打码平台准确率在75%左右。  \n\n![训练集](./pics/traindata.png)\n\n## 3. CNN模型搭建 ##\n\nCNN主要由卷积层，池化层，激活函数组成，再加上一个BatchNorm，BatchNorm叫做批规范化，可以加速模型的收敛速度。\n\n模型代码如下：\n\n\timport torch.nn as nn\n\t\n\tclass CNN(nn.Module):\n    \tdef __init__(self, num_class=36, num_char=4):\n    \t    super(CNN, self).__init__()\n    \t    self.num_class = num_class\n    \t    self.num_char = num_char\n    \t    self.conv = nn.Sequential(\n    \t            #batch*3*180*100\n    \t            nn.Conv2d(3, 16, 3, padding=(1, 1)),\n    \t            nn.MaxPool2d(2, 2),\n    \t            nn.BatchNorm2d(16),\n    \t            nn.ReLU(),\n    \t            #batch*16*90*50\n    \t            nn.Conv2d(16, 64, 3, padding=(1, 1)),\n    \t            nn.MaxPool2d(2, 2),\n    \t            nn.BatchNorm2d(64),\n    \t            nn.ReLU(),\n    \t            #batch*64*45*25\n    \t            nn.Conv2d(64, 512, 3, padding=(1, 1)),\n    \t            nn.MaxPool2d(2, 2),\n    \t            nn.BatchNorm2d(512),\n    \t            nn.ReLU(),\n    \t            #batch*512*22*12\n    \t            nn.Conv2d(512, 512, 3, padding=(1, 1)),\n    \t            nn.MaxPool2d(2, 2),\n    \t            nn.BatchNorm2d(512),\n    \t            nn.ReLU(),\n    \t            #batch*512*11*6\n    \t            )\n    \t    self.fc = nn.Linear(512*11*6, self.num_class*self.num_char)\n    \t    \n    \tdef forward(self, x):\n    \t    x = self.conv(x)\n    \t    x = x.view(-1, 512*11*6)\n    \t    x = self.fc(x)\n    \t    return x\n\nnn.Sequential()可以看作模块的有序容器，可以方便快捷的搭建神经网络。  \n网络的输入是一个shape为`[batch, 3, 180, 100]`的张量，batch代表的是一个批次图片数量，3代表输入的图片是3通道的，即RGB，180和100则分别代表图片的宽和高。  \n\n主要的结构如下：\n\n\n1. 第一个卷积层`nn.Conv2d(3, 16, 3, padding=(1, 1))`，参数分别对应着输入的通道数3，输出通道数16，卷积核大小为3（长宽都为3），padding为（1， 1）可以保证输入输出的长宽不变。shape为`[batch, 3, 180, 100]`的张量通过这个卷积层，输出一个shape为`[batch, 16, 180, 100]`的张量。  \n\n\n2. 接着一个最大池化层`nn.MaxPool2d(2, 2)`，参数分别对应着池化窗口大小为2（长宽都为2），步长为3. 输出的长宽为输入的一半，如果长宽为奇数的话则补边。输入一个shape为`[batch, 16, 180, 100]`的张量，输出为一个shape为`[batch, 16, 90, 50]`的张量。  \n4. 批规范层`nn.BatchNorm2d(16)`，16为输入张量的通道数。  \n5. 激活函数`nn.ReLu()`，就是把小于0的值置0，大于0的值不变，使用激活函数是为了引入非线性，让模型可以拟合更复杂的函数。 \n\n\n经过4组如上结构的卷积后，得到一个shape为`[batch, 512, 11, 6]`的张量，`x.view(-1, 512*11*6)`将改变张量的shape为`[batch, 512*11*6]`，再用一个`[512*11*6, num_class*num_char]`的全连接层映射为一个`[batch, num_class*num_char]`张量，这个就是模型的输出，其中`num_class`代表字符的种类数量，`num_char`代表一张验证码图片含有的字符数量，分别为36与4。  \n\n## 4. 数据加载 ##\npytorch有非常方便高效的数据加载模块--Dataset和DataLoader。  \nDataset是数据样本的封装，可以很方便的读取数据。实现一个Dataset的子类，需要重写`__len__`和`__getitem__`方法，`__len__`需要返回整个数据集的大小，`__getitem__`提供一个整数索引参数，一个样本数据（一个图片张量和一个标签张量）。  \n验证码图片的Dataset代码如下：  \n\n\tclass CaptchaData(Dataset):\n    \tdef __init__(self, data_path, num_class=36, num_char=4, \n    \t             transform=None, target_transform=None, alphabet=alphabet):\n    \t    super(Dataset, self).__init__()\n    \t    self.data_path = data_path\n    \t    self.num_class = num_class\n    \t    self.num_char = num_char\n    \t    self.transform = transform\n    \t    self.target_transform = target_transform\n    \t    self.alphabet = alphabet\n    \t    self.samples = make_dataset(self.data_path, self.alphabet, \n    \t                                self.num_class, self.num_char)\n    \t\n    \tdef __len__(self):\n    \t    return len(self.samples)\n    \t\n    \tdef __getitem__(self, index):\n    \t    img_path, target = self.samples[index]\n    \t    img = img_loader(img_path)\n    \t    if self.transform is not None:\n    \t    \timg = self.transform(img)\n    \t    if self.target_transform is not None:\n            \ttarget = self.target_transform(target)\n        \treturn img, torch.Tensor(target)  \n\n其中`make_dataset`为读取图片路径和标签的函数，返回`[(img_path, target), (img_path, target), ...]`的数据形式。`img_loader`为读取图片的函数，并且转换成RGB三通道。\n这两个函数具体实现如下： \n \n\tdef img_loader(img_path):\n\t    img = Image.open(img_path)\n\t    return img.convert('RGB')\n\t\n\tdef make_dataset(data_path, alphabet, num_class, num_char):\n\t    img_names = os.listdir(data_path)\n\t    samples = []\n\t    for img_name in img_names:\n\t        img_path = os.path.join(data_path, img_name)\n\t        target_str = img_name.split('.')[0]\n\t        assert len(target_str) == num_char\n\t        target = []\n\t        for char in target_str:\n\t            vec = [0] * num_class\n\t            vec[alphabet.find(char)] = 1\n\t            target += vec\n\t        samples.append((img_path, target))\n\t    return samples  \n\nDataLoader是Dataset的进一步封装，Dataset每次通过`__getitem__`方法取到的是一个样本，经过DataLoader封装为dataloader后，每次取的是一个batch大小的样本批次。  \n\n\ttransforms = Compose([ToTensor()])\n\ttrain_dataset = CaptchaData('./data/train', transform=transforms)\n    train_data_loader = DataLoader(train_dataset, batch_size=batch_size, num_workers=0, \n                             shuffle=True, drop_last=True)\n    test_data = CaptchaData('./data/test', transform=transforms)\n    test_data_loader = DataLoader(test_data, batch_size=batch_size, \n                                  num_workers=0, shuffle=True, drop_last=True)\n`transforms`是数据预处理操作，一般数据增强就通过transform实现，可以随机亮度，随机翻转，随机缩放等等。此处只使用了`ToTensor()`，将`PIL.Image`对象转换成Tensor。\n\n## 5. 训练 ##\n\n训练网络的一般流程为：  \n1. 定义网络  \n2. 定义优化器`optimizer`和损失函数`criterion`  \n3. 遍历`dataloader`，每次取一个batch训练。计算loss，将优化器梯度置零，loss向后传播，计算梯度，优化器更新参数。    \n4. 训练集训练完一个epoch后，使用测试集计算下准确率。  \n5. 保存模型  \n主要代码如下：  \n```\n    cnn = CNN()\n    if torch.cuda.is_available():\n        cnn.cuda()\n    optimizer = torch.optim.Adam(cnn.parameters(), lr=base_lr)\n    criterion = nn.MultiLabelSoftMarginLoss()\n    \n    for epoch in range(max_epoch):\n        cnn.train()\n        for img, target in train_data_loader:\n            img = Variable(img)\n            target = Variable(target)\n            if torch.cuda.is_available():\n                img = img.cuda()\n                target = target.cuda()\n            output = cnn(img)\n            loss = criterion(output, target)\n            optimizer.zero_grad()\n            loss.backward()\n            optimizer.step()\n        \n        loss_history = []\n        acc_history = []\n        cnn.eval()\n        for img, target in test_data_loader:\n            img = Variable(img)\n            target = Variable(target)\n            if torch.cuda.is_available():\n                img = img.cuda()\n                target = target.cuda()\n            output = cnn(img)\n            \n            acc = calculat_acc(output, target)\n            acc_history.append(acc)\n            loss_history.append(float(loss))\n        torch.save(cnn.state_dict(), model_path)  \n```\n其中，`cnn.train()`将网络切换到训练状态，`cnn.eval()`将网络切换到模型评估状态，这两者的差别主要体现在dropout和batchnorm层中，模型评估状态下，将不会启用dropout层，batchnrom不会更新均值和标准差。`cnn.cuda()`将数据张量分配到cuda设备上（英伟达显卡），加快运算速度。 \n损失函数使用的是`nn.MultiLabelSoftMarginLoss()`，多分类多标签损失函数。每个类别有多个标签，集每张验证码有四个字符。\n\n选择accuracy（预测准确率）做为模型的评估指标，需要再编写一个计算准确率的函数：  \n\t\n\tdef calculat_acc(output, target):\n    \toutput, target = output.view(-1, 36), target.view(-1, 36)\n    \toutput = nn.functional.softmax(output, dim=1)\n    \toutput = torch.argmax(output, dim=1)\n    \ttarget = torch.argmax(target, dim=1)\n    \toutput, target = output.view(-1, 4), target.view(-1, 4)\n    \tcorrect_list = []\n    \tfor i, j in zip(target, output):\n    \t    if torch.equal(i, j):\n    \t        correct_list.append(1)\n    \t    else:\n    \t        correct_list.append(0)\n    \tacc = sum(correct_list) / len(correct_list)\n    \treturn acc  \n\n训练结果：  \n![训练结果](./pics/training.png)  \n\n最终训练了五十几个epoch后，测试集准确率最高达75%，训练集已过拟合达100%。  \n再将验证码打印出来，预测与实际标签对比：\n![](./pics/display.png)\n\n## 6. 结语 ##\n仅使用800张验证码图片做为训练集，就能最终达到75%的准确率，效果还是比较满意的，已经和打码平台差不多了。要想进一步的提高准确率，需要扩充数据集。可以将已经训练好，准确率达到75%的模型代替打码平台，去获取更多标注好的验证码。数据集充分的情况下，准确率达到90%是比较容易的。  \n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fice-tong%2Fpytorch-captcha","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fice-tong%2Fpytorch-captcha","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fice-tong%2Fpytorch-captcha/lists"}