当我们的训练数据是图像时,我们总是将其放入卷积神经网络中进行训练,也因此卷积网络需要的输入结构为(sample,channels,weight,height),那如果我们是二维表格数据,也可以放入卷积网络中吗?卷积网络常常表现出比普通机器学习算法更为强大的学习能力,如果二维数据结构也可以使用卷积网络来进行处理,或许可以获得更好的结果。首先二维数据基本都储存在csv/txt这些表格结构中,如果我们遇见任意表格结构,可以先将其读入Python:
import numpy as np import torch import matplotlib.pyplot as plt data = np.random.randint(0,255,(10,10000)) #假设现在是10个样本,每个样本10000个特征 data.shape #(10, 10000) data = data.reshape(10,1,100,100) #可以直接使用reshape的方式将数据调整为4维 data = torch.tensor(data) #再放入tensor中转换格式 data #tensor([[[[222, 219, 32, ..., 213, 211, 166], # [204, 119, 26, ..., 148, 57, 23], # [174, 249, 64, ..., 30, 168, 68], # ..., # [246, 223, 4, ..., 112, 123, 76], # [ 27, 35, 95, ..., 40, 192, 251], # [135, 195, 145, ..., 124, 225, 185]]], # # # [[[ 22, 12, 121, ..., 58, 251, 190], # [136, 115, 21, ..., 239, 133, 130], # [ 21, 166, 16, ..., 112, 112, 219], # ..., # [ 31, 19, 15, ..., 150, 50, 130], # [200, 198, 210, ..., 207, 59, 215], # [118, 87, 60, ..., 78, 111, 133]]], # # # [[[245, 80, 245, ..., 125, 26, 143], # [228, 91, 31, ..., 113, 172, 103], # [ 56, 138, 132, ..., 40, 95, 17], # ..., # [115, 22, 72, ..., 189, 21, 117], # [ 61, 86, 65, ..., 81, 182, 141], # [ 78, 56, 35, ..., 180, 41, 99]]], # # # ..., # # # [[[106, 65, 165, ..., 117, 19, 77], # [247, 222, 157, ..., 187, 23, 181], # [188, 110, 136, ..., 140, 44, 108], # ..., # [178, 145, 10, ..., 192, 175, 95], # [187, 88, 145, ..., 160, 150, 87], # [130, 161, 193, ..., 69, 186, 7]]], # # # [[[ 90, 235, 44, ..., 57, 136, 126], # [ 84, 198, 254, ..., 76, 193, 237], # [252, 196, 113, ..., 9, 155, 78], # ..., # [101, 192, 81, ..., 169, 89, 75], # [146, 2, 64, ..., 49, 177, 95], # [118, 21, 155, ..., 179, 165, 20]]], # # # [[[171, 248, 119, ..., 123, 194, 128], # [157, 166, 35, ..., 144, 245, 107], # [ 26, 49, 120, ..., 79, 237, 222], # ..., # [ 61, 207, 5, ..., 118, 118, 137], # [127, 221, 197, ..., 214, 208, 21], # [ 19, 136, 166, ..., 241, 9, 205]]]]) plt.imshow(data.view(-1,100,100,1)[0]); #通道要放在最后一位
reshape并不是一个很难的功能,对于大多数二维数据表而言,真正的问题是没有足够的特征用于变形。大部分二维表格数据的特征数都不是很多,甚至在传统机器学习中,人们害怕高维数据而努力对数据进行降维以提高计算效率。但对卷积神经网络来说,我们至少也需要784(28*28)个特征。那低维数据要怎样才能够放入卷积网络中进行训练呢?答案是先升维,再变化结构。在这里,我介绍一种常见的升维方式:多项式升维。
这是一种将特征数据交互相乘来增加特征维度的方法,它靠增加自变量上的次数来提升维度。只要我们设定一个自变量上的次数(大于1),就可以将特征映射到高维空间、并获得数据投影在高次方的空间中的结果。这种方法可以非常容易地通过sklearn中的类PolynomialFeatures来实现。我们先来简单看看这个类是如何使用的。
class sklearn.preprocessing.PolynomialFeatures (degree=2, interaction_only=False,
include_bias=True)
from sklearn.preprocessing import PolynomialFeatures as PF import numpy as np #如果原始数据是一维的 X = np.arange(1,4).reshape(-1,1) X #array([[1], # [2], # [3]]) #二次多项式,参数degree控制多项式的次方 poly = PF(degree=2) #升维过程当中,你允许我在特征x上设置的最高次方 #接口transform直接调用 X_ = poly.fit_transform(X) X_ #array([[1., 1., 1.], # [1., 2., 4.], # [1., 3., 9.]]) X_.shape #(3, 3) #三次多项式 PF(degree=3).fit_transform(X) #array([[ 1., 1., 1., 1.], # [ 1., 2., 4., 8.], # [ 1., 3., 9., 27.]]) X = np.arange(6).reshape(3,2) X #array([[0, 1], # [2, 3], # [4, 5]]) PF(degree=2).fit_transform(X) #array([[ 1., 0., 1., 0., 0., 1.], # [ 1., 2., 3., 4., 6., 9.], # [ 1., 4., 5., 16., 20., 25.]]) PF(degree=3).fit_transform(X) #array([[ 1., 0., 1., 0., 0., 1., 0., 0., 0., 1.], # [ 1., 2., 3., 4., 6., 9., 8., 12., 18., 27.], # [ 1., 4., 5., 16., 20., 25., 64., 80., 100., 125.]])
很明显,我们可以看出这次生成的数据有这样的规律:
在机器学习课程中,我们曾经证明这样的升维方式可以大幅度提升线性模型处理非线性数据的能力。对于二维表格数据来说,多项式变化能够有效将维度升高。但这种方式并不是所有时候都可以用。多项式变化是对原始特征进行重组后形成新的特征,并没有在原始特征基础上进行深层特征提取,因此当原始特征本来就非常少时,多项式变化非常容易导致过拟合。我们来看一个例子:
#在加利福尼亚房价数据集上做实验 #这是基于sklearn框架的代码,与传统深度学习代码有较大区别 from sklearn.datasets import fetch_california_housing as FCH from sklearn.preprocessing import PolynomialFeatures as PF from sklearn.linear_model import LinearRegression as LR #线性回归 from sklearn.model_selection import train_test_split as TTS from sklearn.metrics import mean_squared_error as MSE data = FCH() #实例化 X = data.data y = data.target y #0~5之间的小数 #array([4.526, 3.585, 3.521, ..., 0.923, 0.847, 0.894]) X.shape #原始数据只有8个特征,特征量非常少 #(20640, 8) y.shape #(20640,) Xtrain,Xtest,Ytrain,Ytest = TTS(X,y,test_size=0.3,random_state=420) reg = LR().fit(Xtrain,Ytrain) MSE(reg.predict(Xtrain),Ytrain) #0.5218522662533102 MSE(reg.predict(Xtest),Ytest) #训练集和测试集结果非常相近,虽然表现不佳但是不存在太多过拟合的情况 #0.5309012639324552 poly = PF(degree=4).fit(Xtrain) Xtrain_ = poly.transform(Xtrain) Xtest_ = poly.transform(Xtest) Xtrain_.shape #(14448, 495) Xtest_.shape #(6192, 495) reg = LR().fit(Xtrain_,Ytrain) MSE(reg.predict(Xtrain_),Ytrain) #0.3046198739854925 MSE(reg.predict(Xtest_),Ytest) #测试集上的MSE变得巨大无比,这是严重过拟合的情况 #258167.53146340803
因此,在我们对数据进行升维、并考虑将数据放入卷积网络进行学习时,必须要考虑数据本身的复杂程度是否足够。当数据过于简单、特征量过少的时候,多项式操作只能加重过拟合,卷积网络对于这样的数据来说也是过于复杂的、并不经济的模型。我们可以通过升维后的表现来判断数据是否具有更强大的潜力(即,特征本身含有较多信息,在升维之后也不会那么容易过拟合,比较适合放入卷积网络进行训练)。来看这一组数据:
from sklearn.datasets import fetch_covtype as FC from sklearn.preprocessing import PolynomialFeatures as PF from sklearn.linear_model import LogisticRegression as LR #逻辑回归 from sklearn.model_selection import train_test_split as TTS data = FC() #首次加载会比较耗时,需要进行数据下载 data.data.shape #数据量巨大,因此我们从中抽样2000个样本来进行训练 #(581012, 54) data.target #array([5, 5, 2, ..., 3, 3, 3]) X = data.data[:2000] X.shape #(2000, 54) np.unique(y) #array([1, 2, 3, 4, 5, 6, 7]) y = data.target[:2000] Xtrain,Xtest,Ytrain,Ytest = TTS(X,y,test_size=0.3,random_state=420) #======【TIME WARNING:1mins】=======# clf = LR(random_state=420, max_iter=1000,solver="newton-cg").fit(Xtrain,Ytrain) clf.score(Xtrain,Ytrain) #对分类模型而言,该接口是分类准确率 #0.7528571428571429 clf.score(Xtest,Ytest) #0.7116666666666667 poly = PF(degree=2,interaction_only=True).fit(Xtrain) #不包含各特征的平方项 Xtrain_ = poly.transform(Xtrain) Xtest_ = poly.transform(Xtest) Xtrain_.shape #(1400, 1486) np.sqrt(1486) -> 38x38,39x39 #38.54867053479276 1486/39 #38.1025641025641 #======【TIME WARNING:10mins】======# clf_ = LR(random_state=420, max_iter=1000,solver="newton-cg").fit(Xtrain_,Ytrain) clf_.score(Xtrain_,Ytrain) #0.8321428571428572 clf_.score(Xtest_,Ytest) #准确率表现整体上升,存在过拟合但没有太夸张,数据适合放入卷积网络 #0.7683333333333333
还有很多其他的升维方法,但多项式是既不改变原有特征、又明确能够一定程度上提升模型表现的方法之一,它唯一的缺点就是无法规定具体升维至多少维度。在卷积网络中,我们需要将特征转化为两数相乘的结构,其中又以偶数x偶数为最佳(各大经典架构内部都有不断将特征图尺寸折半的操作,因此偶数、或除以2之后会变为偶数的值是最佳的)。因此升维之后需要进一步确定最终的总特征量。(如果你使用的是输入具体维度来进行升维的升维方法,那你需要在升维之前就确定好你最终需要的总特征量是多少)。现在我们总共有1486个特征,我们既可以调整成宽>高,例如52x28的形式,也可以调整为两数相等,取 ,即38x38的形式。但无论如何,我们只能够选择相乘后的总特征量小于1486的结构(在这个约束下,信息损失最小的结构是39x38)。
当然,我们可以选择将特征信息保留最多的形式,例如,令结构为39x38,可最大程度上保留信息。当输入卷积网络后,再使用图像预处理的方式进行特征筛选。但图像处理上的特征筛选大部分是随机的,比较适用于图像数据,却不太适用于表格数据,因此最好的方法还是我们手动筛选特征。假设我们现在,剔除42个特征,令特征总量下降到1444(38x38),然后再将数据整理为四维结构。那我们怎么剔除这42个特征呢?我们利用逻辑回归的权重进行筛选。逻辑回归和线性回归一样,本质上都是单层神经网络,他们的权重可以被追溯到具体的特征值上,而多层神经网络的权重却无法被追溯。因此单层神经网络的权重可以被用来衡量特征的重要性。我们可以令逻辑回归的权重从高到低进行排列,并删掉对模型影响最小的42个特征,再将剩下的特征调整为四维:
clf_.coef_.shape #(7, 1486) weights = pd.DataFrame(abs(clf_.coef_).mean(axis=0)) idx = weights.sort_values(by=0,ascending=False).iloc[:1444,0].index X_ = poly.transform(X) X_ = X_[:,idx] X_.shape #(2000, 1444) X_ = X_.reshape(2000,1,38,38)
当我们已经获得了四维的数据后,我们就可以使用之前学过的TensorDataset将数据打包了:
#如何将我们的特征高维矩阵与我们的标签结合起来
from torch.utils.data import TensorDataset
y.shape
#(2000,)
data = TensorDataset(torch.tensor(X_),torch.tensor(y))
for x,y in data:
print(x.shape)
print(y)
break
#torch.Size([1, 38, 38])
#tensor(5, dtype=torch.int32)
最终生成的结果就是我们的数据,其结构与我们直接从torchvision.datasets读出来的内容非常相似。现在只要将数据放入random_split就可以分割训练集测试集,再放入DataLoader分割批次,就可以导入训练了。
2.3 从mat/pt/lmdb到四维tensor除了二维表格数据和图片数据,我们还可能遇见其他各种各样的数据格式,常见的有matlab中导出的.mat格式,储存图片的pt格式,caffe中常用的数据库格式lmdb等。不同格式需要使用不同的方式进行导入,其中mat格式与pt格式较为简单,具体如下所示:
#FashionMNIST数据集就是pt格式的数据,可以直接使用torch.load进行读取 #import torch X, y = torch.load(r"F:datasetsFashionMNISTprocessedtest.pt") X.shape y #mat格式,SVHN就是mat格式数据集,我们使用scipy中的sio模块进行读取 #通常来说,scipy属于anaconda自带库,无需额外安装。如果你需要安装scipy,搜索pip安装scipy即可 import scipy.io as sio #import numpy as np #import torch loaded_mat = sio.loadmat(r'F:datasetsSVHNtrain_32x32.mat') X = torch.tensor(loaded_mat['X']) y = loaded_mat['y'].astype(np.int64).squeeze() #格式与pytorch要求的不符,记得调整 X.shape X = X.reshape(-1,3,32,32) #(sample, channels, weight, height)
lmdb格式文件大多出现在早年的数据中,大部分用于读取lmdb文件的代码也年久失修,就连github上的众多代码也不能顺利跑通,因此我对PyTorch源码稍作修改,构造了用于读取单一的lmdb文件的类ImageFolderLMDB 。通常当我们的数据储存为lmdb格式时,每个lmdb文件中都是单一的标签类别,因此ImageFolderLMDB中会允许我们输入这个lmdb文件中的标签类别。当我们需要多个类别或多个imdb文件时,我们可以使用ImageFolderLMDB多次读取出不同的数据,然后使用torch.utils.data中的ConcatDataset 类将不同的数据集合并起来。从代码的角度来说,这段代码还有非常多可以优化的地方(有许多可能的不规范输入没有被限制,当不规范输入发生时,我也没注明有指导意义的报错信息),但限于课时和时间,我们只能将其修缮到能够顺利跑通并执行完整任务的程度。之后我们会持续迭代具体的代码。
#本段代码已超出深度学习范围,仅供使用,不做讲解。
#如果你希望,可以将其保存在torchlearning.py文件中方便导入
import os
os.environ["KMP_DUPLICATE_LIB_OK"]="TRUE"
import six
import string
import pickle
import bisect
import lmdb
from PIL import Image
import torch
from torch.utils.data import DataLoader, Dataset, IterableDataset
from torchvision.transforms import transforms
from torchvision.datasets import ImageFolder
from torchvision import transforms, datasets
class ImageFolderLMDB(Dataset):
"""
用于从单一lmdb文件中提取出数据集的类
只适用于lmdb文件中只包含一个标签类别的情况
不同的标签类别需要使用不同的ImageFolderLMDB进行提取
"""
def __init__(self, db_path, classes: int, transform=None, target_transform=None):
"""
参数说明
db_path: 字符串,需要读取的lmdb文件所在的根目录
classes: int,给现有数据集打上的单一标签。注意该标签是人工标注的,不一定是数据中心的客观标签
"""
super().__init__()
self.db_path = db_path
self.classes = classes
self.transform = transform
self.target_transform = target_transform
#首先使用lmdb库从lmdb文件中提出数据集
self.env = lmdb.open(db_path, max_readers=1, readonly=True, lock=False,
readahead=False, meminit=False)
with self.env.begin(write=False) as txn:
self.length = txn.stat()['entries']
cache_file = '_cache_' + ''.join(c for c in db_path if c in string.ascii_letters)
if os.path.isfile(cache_file):
self.keys = pickle.load(open(cache_file, "rb"))
else:
with self.env.begin(write=False) as txn:
self.keys = [key for key in txn.cursor().iternext(keys=True, values=False)]
pickle.dump(self.keys, open(cache_file, "wb"))
def __getitem__(self, index):
img, target = None, None
env = self.env
with env.begin(write=False) as txn:
imgbuf = txn.get(self.keys[index])
# 导入图像
buf = six.BytesIO()
buf.write(imgbuf)
buf.seek(0)
img = Image.open(buf).convert('RGB')
# 导入标签
target = self.classes
if self.transform is not None:
img = self.transform(img)
if self.target_transform is not None:
target = self.target_transform(target)
return img, target
def __len__(self):
return self.length
def __repr__(self):
return self.__class__.__name__ + ' (' + self.db_path + ')'
我们在LSUN数据集上试试这两个类:
from torch.utils.data import ConcatDataset
#先使用ImageFolderLMDB单独读取数据中的两个类别
data_church = ImageFolderLMDB(r"F:datasets2lsun-masterdatachurch_outdoor_train_lmdb",classes=0
,transform=transforms.ToTensor())
data_church[0][0].shape
for x,y in data_church:
print(x.shape)
print(y)
break
data_classroom = ImageFolderLMDB(r"I:F盘 + 代码F盘datasets2lsunmasterdataclassroom_train_lmdb",classes=1
,transform=transforms.ToTensor())
data_classroom[0][0].shape
#使用ConcatDataset将其合并
data = ConcatDataset([data_church,data_classroom])
#数据尺寸已经超出了可以做循环的程度
data.__len__()
data_church.__len__()
data_classroom.__len__()
data[120000]
data[160000]
在实际中,我们还可能遇见各种各样的数据格式。如果你遇见新的数据格式,可以先谷歌搜索"xxx to pytorch tensor",如果无果,你可以试着搜索"xxx to csv"或’'xxx to png"。只要能够将数据集提取为图片或表格,导入过程就会变得容易很多。如果你还遇见过其他导入数据的形式,并且你成功实现了数据的导入,也欢迎在交流群内进行分享。



