skyline R34与R35分类器---第一次尝试

skyline R34与R35分类器—第一次尝试

本篇旨在记录我第一次基于薄弱的理论基础,通过自己获取skyline图像数据集,企图实现R34 与 R35的二分类,最终效果一般(很多地方未做优化,尤其是数据集),但希望可以先经过自己的思考并记录下来,以便日后能够不断完善处理识别任务的流程框架。

1.数据集获取

想做一个这样的分类器,么的数据集该怎么办,迫不得已写个爬虫。

本次选择爬取百度图片,由于太长时间不写爬虫了,被反爬机制折腾了半天。不过好在最终还是顺利地批量下载下来了,但数量有限,R34和R35的汽车图片,未经清洗的数据集加起来也只有2500。手工删除了一些很扯的图片后,数据量减少了五倍…😭。

(1)获取url列表(GTRGTRGTRGTRGTRGTRGTRGTRGTR!)
在这里插入图片描述
一张一张的手动下载非常低效,按下F12键,打开chrome的开发者工具,如下图所示:
在这里插入图片描述

ctrl+r键 刷新页面,现在百度图片看不到可以选择指定页的按钮,鼠标滚轮往下会自动更新图片,一次更新30张(从后面的json数据可以看出来),从自动刷新的现象和抓取的数据包可以看出,它使用了ajax来异步刷新部分网页页面,点击这个每次往下滑都会收到的数据包,获取URL,如下所示:在这里插入图片描述
这个URL比较冗长,包含了很多多余的(对我来说)请求字符串参数,实际上精简一下效果一样, 并且似乎直接用这个较长的URL会触发反爬,下载下来的图片都打不开。经过试错,精简后的URL是这样的:
https://image.baidu.com/search/acjson?tn=resultjson_com&ipn=rj&rn=30&word=GT-R35&pn=30
然后找规律即可,可以发现rn=30代表一次性刷新多少图片,pn代表从第几张图片开始继续加载,根据这样的规律,我们可以写一个url列表,用于存储所有我需要的url。比如搜索R34时,当我pn设置为1800左右,通过该类url获取的response中就不提供图片数据了,因此需要根据这种情况来限制pn的大小:代码比较简单,如下所示:

def Url_generator():
        Query_String_Parameters_pn ={
            'pn': 0,
        }
        url_list = []
        for i in tqdm(range(60, 1801, 30)):
            Query_String_Parameters_pn['pn'] = i
            url =  'https://image.baidu.com/search/acjson?tn=resultjson_com&ipn=rj&rn=30&word=GT-R34&'+ urlencode(Query_String_Parameters_pn)
            print(url)
            url_list.append(url)
            #print(i)
            
            
        return url_list

结果如下所示

100%|██████████| 59/59 [00:00<00:00, 19621.31it/s]
https://image.baidu.com/search/acjson?tn=resultjson_com&ipn=rj&rn=30&word=GT-R35&pn=60
https://image.baidu.com/search/acjson?tn=resultjson_com&ipn=rj&rn=30&word=GT-R35&pn=90
https://image.baidu.com/search/acjson?tn=resultjson_com&ipn=rj&rn=30&word=GT-R35&pn=120
https://image.baidu.com/search/acjson?tn=resultjson_com&ipn=rj&rn=30&word=GT-R35&pn=150
https://image.baidu.com/search/acjson?tn=resultjson_com&ipn=rj&rn=30&word=GT-R35&pn=180
.......

(2)批量获取响应内容
下面可以获取响应内容,可以先随便复制一个上图的url,用浏览器看一哈,显示的json比较混乱。

在这里插入图片描述
随便使用一个json在线解析网站,解析上面看起来乱七八糟的json数据,结果如下所示:在这里插入图片描述
在这里插入图片描述
这里看到每次加载了30个object,每个object里面都有许多url, 这里在每个object里都选择thumbURL作为每个图片的url。现在我们可以有的资源包括url列表,以及发出每个请求后获得响应内容中的每张图片的url。因此,可以开始写爬虫代码了,记得写header,伪装成浏览器。

首先通过之前的url_list, 获取所有图片的url, 代码如下:

import json
import time
def get_imgList(url_list):
    img_list = []
    headers = {
        'Host':'image.baidu.com',
        'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36',
        'Accept':'text/plain, */*; q=0.01',
        'X-Requested-With':'XMLHttpRequest',
        'Referer':'http://image.baidu.com/search/index?tn=baiduimage&ps=1&ct=201326592&lm=-1&cl=2&nc=1&ie=utf-8&word=GT-R35'}
    for url in tqdm(url_list):
        response = requests.get(url,headers)
        text = response.text
        #print(text)
        try:
            
            jsonData =  json.loads(text)
            if 'data' in jsonData.keys():
                #print(len(jsonData['data']))
                for items in jsonData['data']:
                    if 'thumbURL' in items:
                        #print(items[thumbURL])
                        img_list.append(items['thumbURL'])
                    else:
                        continue
        except:
            #print('something is wrong')
            continue
        
        time.sleep(2)
    return img_list

(3)爬取数据
获取了图像列表后,就可以不断地发出request,下载图片,保存在本地文件夹中了。

def img_DownLoad(img_list, headers):
    for i in tqdm(range(0, len(img_list))):
        response = requests.get(img_list[i], headers)
        img = response.content
        with open(f'skyline_image/R35/{i}.jpg', 'wb') as f:
            f.write(img)

数据获取部分结束,经过粗略的挑选,能用的总共仅有600张有余。

2.处理数据集

(1)加载图像数据
现在已经获取了R34 和R35的数据,并保存在文件夹中
在这里插入图片描述
下面就可以使用opencv读取数据并将图片数据和标签一同存储在numpy数组中,最后保存为npy格式文件,方便以后再一次读取。

import os 
import cv2
import numpy as np
from tqdm import tqdm
REBUILD_DATA = False
class R34VSR35():
    IMG_SIZE = 224 # 设定图像大小为224 * 224
    R34 = "skyline_image/R34_clean"
    R35 = "skyline_image/R35_clean"
    LABELS = {R34: 0, R35: 1} #用于设置标签
    training_data = []
    R34count = 0
    R35count = 0
    def make_training_data(self):
        for label in self.LABELS:
            print(label)
            for f in tqdm(os.listdir(label)):
                try:
                    path = os.path.join(label, f)
                    img = cv2.imread(path) # (374, 500, 3) # 读取三通道图像数据
                    img = cv2.resize(img,(self.IMG_SIZE, self.IMG_SIZE))
                    
                    self.training_data.append([np.array(img), np.eye(2)[self.LABELS[label]]])
                    if label == self.R34:
                        self.R34count += 1 #统计每个种类图像的个数
                    elif label == self.R35:
                        self.R35count += 1
                        
                except Exception as e:
                    pass
        np.random.shuffle(self.training_data)
        np.save("training_data_skyline.npy", self.training_data)#保存到npy格式文件中
        print("R34: ", self.R34count)
        print("R35:", self.R35count)

保存成npy格式文件后,以后就可以方便地读取数据集到一个多维数组当中了。

读取该文件的代码如下

import numpy as np
training_data_skyline = np.load("training_data_skyline.npy", allow_pickle=True)

training_data_skyline[x][0],x=0, 1, 2, 3 , …, n,的shape都是(224,224,3)
training_data_skyline[x][1],x=0, 1, 2, 3 , …, n,的shape都是(2,)

matplotlib可以查看指定的图像。

import matplotlib.pyplot as plt
from jupyterthemes import jtplot
jtplot.style()
%matplotlib notebook
print(np.shape(training_data_skyline[0][0]))
plt.imshow(training_data_skyline[6][0])
training_data_skyline[6][1]

在这里插入图片描述

3.模型

数据有了之后,可以考虑模型的问题,这次直接使用了resnet18,并且稍加改造,让他适应二分类的需求,将原本最后一层的全连接层输入之后加入256输出单元的全连接层,再链接一个ReLU层和Dropout层,然后添加2个单元的全连接层,最后通过一个softmax层输出。

import torch
import torchvision
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
res18 = torchvision.models.resnet18(pretrained=True)
for param in res18.parameters():
    param.requires_grad = False
fc_inputs = res18.fc.in_features
res18.fc = nn.Sequential(
    nn.Linear(fc_inputs, 256),
    nn.ReLU(),
    #nn.Dropout(0.4),
    nn.Linear(256, 2),
    nn.LogSoftmax(dim=1)
)

查看网络结构可以看到修改成功。

ResNet(
  (conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
  (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  (relu): ReLU(inplace=True)
  (maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
  (layer1): Sequential(
    (0): BasicBlock(
      (conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    )
    (1): BasicBlock(
      (conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    )
  )
  (layer2): Sequential(
    (0): BasicBlock(
      (conv1): Conv2d(64, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (downsample): Sequential(
        (0): Conv2d(64, 128, kernel_size=(1, 1), stride=(2, 2), bias=False)
        (1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (1): BasicBlock(
      (conv1): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    )
  )
  (layer3): Sequential(
    (0): BasicBlock(
      (conv1): Conv2d(128, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (downsample): Sequential(
        (0): Conv2d(128, 256, kernel_size=(1, 1), stride=(2, 2), bias=False)
        (1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (1): BasicBlock(
      (conv1): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    )
  )
  (layer4): Sequential(
    (0): BasicBlock(
      (conv1): Conv2d(256, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (downsample): Sequential(
        (0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)
        (1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (1): BasicBlock(
      (conv1): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    )
  )
  (avgpool): AdaptiveAvgPool2d(output_size=(1, 1))
  (fc): Sequential(
    (0): Linear(in_features=512, out_features=256, bias=True)
    (1): ReLU()
    (2): Linear(in_features=256, out_features=2, bias=True)
    (3): LogSoftmax()
  )
)

4.将数据放入tensor

接着将数据和标签喂给tensor,由于个人配置很一般,选择了最稳的办法,一次喂25个图像给tensor。

y = torch.Tensor([i[1] for i in training_data_skyline[0:500]])
print(len(y))
print(np.shape(y))
#X = torch.Tensor([i[0] for i in tqdm(training_data_1)]).view(-1,3, 224,224)
#x = torch.empty(size=[1, 224, 224, 3])
X_initial = torch.Tensor([i[0] for i in training_data_skyline[0:25]]).view(-1, 224, 224, 3)
for j in tqdm(range(25, 500, 25)):
        if j == 25:
            X = torch.cat((X_initial, torch.Tensor([i[0] for i in training_data_skyline[j:j+25]]).view(-1, 224, 224, 3)), dim=0)
        else: X = torch.cat((X, torch.Tensor([i[0] for i in training_data_skyline[j:j+25]]).view(-1, 224, 224, 3)), dim=0)
        print(np.shape(X))
X = X/255.0

标签倒是无所谓,直接一次性丢进tensor,之后对X进行归一化,将图像像素强度控制在0-255之间。

接着按照1:10比例划分测试集与训练集(没划分验证集嘤嘤嘤),这里设置数据集一共500个,50张测试集,450张训练集,已经打乱图片数据的顺序,把y进行相应划分。

VAL_PCT = 0.1 
VAL_size = int(len(X)*VAL_PCT)
train_x = X[:-VAL_size]
train_y = y[:-VAL_size]
test_x = X[-VAL_size:]
test_y = y[-VAL_size:]

设定每批25个图像丢进模型中, 迭代10次(可能太多了),并准备记录训练集准确率和测试集准确率。为了节省训练时间,使用GPU加速,这里已经安装了CUDA和cuDNN,可以在配置好之后检查自己的GPU是否可用。

BATCH_SIZE = 25
EPOCHS = 3
torch.cuda.device_count()
if torch.cuda.is_available():
    device = torch.device("cuda:0")
    print("running on the GPU")
else:
    device = torch.device("cpu")
    print("running on the CPU")
res18.to(device)

设置优化器和损失函数,这里用了均方误差函数而没有用交叉熵,在此建议分类问题偏向于使用交叉熵,如二分类的交叉熵损失BCELoss。使用adam优化算法代替SGD。

optimizer = optim.Adam(res18.parameters(), lr= 0.005)
loss_function = nn.MSELoss()

5.训练及测试

写一个测试和训练都适用的一部分流程,只有当train参数为true时,才清空梯度累积;记录损失和准确率,接着当train为true时,才进行反向传播,应用我们的优化器。最后返回准确率和损失值。

def fwd_pass(x, y, train = False):
    if train:
        optimizer.zero_grad()
    outputs = res18(x)
    matches = [torch.argmax(i) == torch.argmax(j) for i, j in zip(outputs, y)]
    acc = matches.count(True)/len(matches)
    loss = loss_function(outputs, y)
    if train:
        loss.backward()
        optimizer.step()
    return acc, loss

然后是分批次训练及测试过程,并及时记录准确率及损失,写入log文件中,方便查看训练情况并优化模型。

import time
def test(size = 25):
    random_start = np.random.randint(len(test_x)-size)
    x, y = test_x[random_start:random_start+size], test_y[random_start:random_start+size]
    with torch.no_grad():
        val_acc, val_loss = fwd_pass(x.view(-1, 3, 224, 224).to(device), y.to(device))
    return val_acc, val_loss

MODEL_NAME = f"modelskyline-{int(time.time())}"
print(MODEL_NAME)
def train():
    BATCH_SIZE = 25
    EPOCHS = 10
    best_train_acc = 0.0
    best_val_acc = 0.0
    with open("model_skyline.log","a") as f:
        for epoch in range(EPOCHS):
            for i in tqdm(range(0, len(train_x), BATCH_SIZE)):
                batch_x = train_x[i:i+BATCH_SIZE].view(-1, 3, 224, 224).to(device)
                batch_y = train_y[i:i+BATCH_SIZE].to(device)
                acc, loss = fwd_pass(batch_x, batch_y, train=True)
                if best_train_acc < acc:
                    best_train_acc = acc
                if i % 25 == 0:
                    val_acc,  val_loss = test(size=5)
                    if best_val_acc < val_acc:
                        best_val_acc = val_acc
                    f.write(f"{MODEL_NAME}, {round(time.time(), 3)}, {round(float(acc),2)}, {round(float(loss), 2)},  {round(float(val_acc),2)}, {round(float(val_loss), 2)},  {round(float(best_train_acc),2)}, {round(float(best_val_acc), 2)}\n")
                

训练情况如下所示
在这里插入图片描述
训练结果还好,但是识别测试集的结果一般,有过拟合情况- -,惭愧,时间有限先立个flag,日后慢慢优化。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值