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,日后慢慢优化。