学习了沐神的 gluon 课程,觉得里面有关于 fcn 的课程 特别有用,于是总结一下,同时使用 pytorch 重新实现,不仅实现 gluon 教程中的部分,同时实现论文中更精细的形式。

介绍

语义分割是一种像素级别的处理图像方式,对比于目标检测其更加精确,能够自动从图像中划分出对象区域并识别对象区域中的类别,比如下面这个效果

上面是输入的图片,下面是希望得到的效果,也就是希望能够对区域进行像素级别的划分

在 2015 年 CVPR 的一篇论文 Fully Convolutional Networks for Semantic Segmentation 这篇文章提出了全卷积的概念,第一次将端到端的卷积网络推广到了语义分割的任务当中,随后出现了很多基于 FCN 实现的网络结构,比如 U-Net 等。

数据集

首先我们需要下载数据集,这里我们使用 PASCAL VOC 数据集,其是一个正在进行的目标检测,目标识别,语义分割的挑战,我们可以进行数据集的下载

下载完成数据集之后进行解压,我们可以在 ImageSets/Segmentation/train.txt 和 ImageSets/Segmentation/val.txt 中找到我们的训练集和验证集的数据,图片存放在 /JPEGImages 中,后缀是 .jpg,而 label 存放在 /SegmentationClass 中,后缀是 .png

我们可以可视化一下

首先输出图片的大小,左边就是真实的图片,右边就是分割之后的结果

然后我们定义一个函数进行图片的读入,根据 train.txt 和 val.txt 中的文件名进行图片读入,我们不需要这一步就读入图片,只需要知道图片的路径,之后根据图片名称生成 batch 的时候再读入图片,并做一些数据预处理

voc_root = './data/VOCdevkit/VOC2012'

def read_images(root=voc_root, train=True):

txt_fname = root + '/ImageSets/Segmentation/' + ('train.txt' if train else 'val.txt')

with open(txt_fname, 'r') as f:

images = f.read().split()

data = [os.path.join(root, 'JPEGImages', i+'.jpg') for i in images]

label = [os.path.join(root, 'SegmentationClass', i+'.png') for i in images]

return data, label

数据预处理

可能你已经注意到了前面展示的两张图片的大小是不一样的,如果我们要使用一个 batch 进行计算,我们需要图片的大小保持一致,在前面使用卷积网络进行图片分类的任务中,我们通过 resize 的办法对图片进行了缩放,使得他们的大小相同,但是这里会遇到一个问题,对于输入图片我们当然可以 resize 成任意我们想要的大小,但是 label 也是一张图片,且是在 pixel 级别上的标注,所以我们没有办法对 label 进行有效的 resize 似的其也能达到像素级别的匹配,所以为了使得输入的图片大小相同,我们就使用 crop 的方式来解决这个问题,也就是从一张图片中 crop 出固定大小的区域,然后在 label 上也做同样方式的 crop。

使用 crop 可以使用 pytorch 中自带的 transforms,不过要稍微改一下,不仅输出 crop 出来的区域,同时还要输出对应的坐标便于我们在 label 上做相同的 crop

def rand_crop(data, label, height, width):

'''data is PIL.Image objectlabel is PIL.Image object'''

data, rect = tfs.RandomCrop((height, width))(data)

label = tfs.FixedCrop(*rect)(label)

return data, label

下面我们可以验证一下随机 crop

上面就是我们做两次随机 crop 的结果,可以看到图像和 label 能够完美的对应起来

接着我们根据数据知道里面有 21 中类别,同时给出每种类别对应的 RGB 值

classes = ['background','aeroplane','bicycle','bird','boat',

'bottle','bus','car','cat','chair','cow','diningtable',

'dog','horse','motorbike','person','potted plant',

'sheep','sofa','train','tv/monitor']

# RGB color for each class

colormap = [[0,0,0],[128,0,0],[0,128,0], [128,128,0], [0,0,128],

[128,0,128],[0,128,128],[128,128,128],[64,0,0],[192,0,0],

[64,128,0],[192,128,0],[64,0,128],[192,0,128],

[64,128,128],[192,128,128],[0,64,0],[128,64,0],

[0,192,0],[128,192,0],[0,64,128]]

len(classes), len(colormap)

接着可以建立一个索引,也就是将一个类别的 RGB 值对应到一个整数上,通过这种一一对应的关系,能够将 label 图片变成一个矩阵,矩阵和原图片一样大,但是只有一个通道数,也就是 (h, w) 这种大小,里面的每个数值代表着像素的类别

cm2lbl = np.zeros(256**3) # 每个像素点有 0 ~ 255 的选择,RGB 三个通道

for i,cm in enumerate(colormap):

cm2lbl[(cm[0]*256+cm[1])*256+cm[2]] = i # 建立索引

def image2label(im):

data = np.array(im, dtype='int32')

idx = (data[:, :, 0] * 256 + data[:, :, 1]) * 256 + data[:, :, 2]

return np.array(cm2lbl[idx], dtype='int64') # 根据索引得到 label 矩阵

定义完成之后,我们可以验证一下

label_im = Image.open('./data/VOCdevkit/VOC2012/SegmentationClass/2007_000033.png').convert('RGB')

label = image2label(label_im)

label[150:160, 240:250]

array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],

[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],

[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],

[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],

[1, 0, 0, 0, 0, 0, 0, 0, 0, 0],

[1, 1, 1, 1, 1, 0, 0, 0, 0, 0],

[1, 1, 1, 1, 1, 1, 1, 1, 0, 0],

[1, 1, 1, 1, 1, 1, 1, 1, 1, 1],

[1, 1, 1, 1, 1, 1, 1, 1, 1, 1],

[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]])

可以看到上面的像素点由 0 和 1 构成,0 表示背景,1 表示 飞机这个类别

接着我们可以定义数据预处理方式,之前我们读取的数据只有文件名,现在我们开始做预处理,非常简单,首先随机 crop 出固定大小的区域,然后使用 ImageNet 的均值和方差做标准化。

def img_transforms(im, label, crop_size):

im, label = rand_crop(im, label, *crop_size)

im_tfs = tfs.Compose([

tfs.ToTensor(),

tfs.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])

])

im = im_tfs(im)

label = image2label(label)

label = torch.from_numpy(label)

return im, label

最后我们定义一个 COVSegDataset 继承于 `torch.utils.data.Dataset` 构成我们自定的训练集

class VOCSegDataset(Dataset):

'''voc dataset'''

def __init__(self, train, crop_size, transforms):

self.crop_size = crop_size

self.transforms = transforms

data_list, label_list = read_images(train=train)

self.data_list = self._filter(data_list)

self.label_list = self._filter(label_list)

print('Read ' + str(len(self.data_list)) + ' images')

def _filter(self, images): # 过滤掉图片大小小于 crop 大小的图片

return [im for im in images if (Image.open(im).size[1] >= self.crop_size[0] and

Image.open(im).size[0] >= self.crop_size[1])]

def __getitem__(self, idx):

img = self.data_list[idx]

label = self.label_list[idx]

img = Image.open(img)

label = Image.open(label).convert('RGB')

img, label = self.transforms(img, label, self.crop_size)

return img, label

def __len__(self):

return len(self.data_list)

# 实例化数据集

input_shape = (320, 480)

voc_train = VOCSegDataset(True, input_shape, img_transforms)

voc_test = VOCSegDataset(False, input_shape, img_transforms)

train_data = DataLoader(voc_train, 64, shuffle=True, num_workers=4)

valid_data = DataLoader(voc_test, 128, num_workers=4)

```

fcn 模型

fcn 模型非常简单,里面全部是由卷积构成的,所以被称为全卷积网络,同时由于全卷积的特殊形式,因此可以接受任意大小的输入,网络的示意图如下

对于任何一张输入图片,由于卷积层和池化层的不断作用,得到的输出形状会越来越小,但是通道数会越来越大,比如 ResNet18,会将输入的长宽减小 32 倍,由 3x244x244 变成 512x7x7,也就是上图的第一部分,得到的特征图会特别小,最后通过一个转置卷积得到一个和输入形状一样的结果,这里我们引入了转置卷积的概念,下面我们讲一讲什么是转置卷积

转置卷积

我们首先可以看看下面的动画

第一张就是我们常说的卷积的效果,而转置卷积就是下面这个操作,相当于卷积的逆过程,将卷积的输入和输出反过来,卷积的正向传播相当于图片左乘一个矩阵 c,反向传播相当于左乘 $c^T$,而转置卷积的正向过程相当于左乘 $c^T$,反向过程相当于左乘 $(c^T)^T = c$,详细的推导可以看看论文

而转置卷积的计算公式也非常简单,对于卷积

转置卷积就是将输入和输出反过来,即

如果我们希望输出变成输入的两倍,那么 stride 取 2,kernel 和 padding 可以对应着取,比如 kernel 取 4,那么 padding 就取 1

在 pytorch 中转置卷积可以使用 torch.nn.ConvTranspose2d() 来实现,下面我们举个例子

x = torch.randn(1, 3, 120, 120)

conv_trans = nn.ConvTranspose2d(3, 10, 4, stride=2, padding=1)

y = conv_trans(Variable(x))

print(y.shape)

torch.Size([1, 10, 240, 240])

可以看到输出变成了输入的 2 倍

模型结构

最简单的 fcn 前面是一个去掉全连接层的预训练网络,然后将去掉的全连接变为 1x1 的卷积,输出和类别数目相同的通道数,比如 voc 数据集是 21 分类,那么输出的通道数就是 21,然后最后接一个转置卷积将结果变成输入的形状大小,最后在每个 pixel 上做一个分类问题,使用交叉熵作为损失函数就可以了。

当然这样的模型是特别粗糙的,因为最后一步直接将图片扩大了 32 倍,所以论文中有一个改进,就是将网络中间的输入联合起来进行转置卷积,这样能够依赖更多的信息,所以可以得到更好的结果,可以看看下面的图示

fcn-32s 就是直接将最后的结果通过转置卷积扩大 32 倍进行输出,而 fcn-16x 就是联合前面一次的结果进行 16 倍的输出,fcn-8x 就是联合前面两次的结果进行 8 倍的输出,我们用上图中 fcn-8x 举例,就是先将最后的结果通过转置卷积扩大 2 倍,然后和 pool4 的结果相加,然后在通过转置卷积扩大 2 倍,然后和 pool3 的结果相加,最后通过转置卷积扩大 8 倍得到和输入形状一样大的结果。

bilinear kernel

通常我们训练的时候可以随机初始化权重,但是在 fcn 的网络中,使用随机初始化的权重将会需要大量的时间进行训练,所以我们卷积层可以使用在 imagenet 上预训练的权重,那么转置卷积我们使用什么样的初始权重呢?这里就要用到 bilinear kernel。

我们可以看看下面的例子

# 定义 bilinear kernel

def bilinear_kernel(in_channels, out_channels, kernel_size):

'''return a bilinear filter tensor'''

factor = (kernel_size + 1) // 2

if kernel_size % 2 == 1:

center = factor - 1

else:

center = factor - 0.5

og = np.ogrid[:kernel_size, :kernel_size]

filt = (1 - abs(og[0] - center) / factor) * (1 - abs(og[1] - center) / factor)

weight = np.zeros((in_channels, out_channels, kernel_size, kernel_size), dtype='float32')

weight[range(in_channels), range(out_channels), :, :] = filt

return torch.from_numpy(weight)

x = Image.open('./data/VOCdevkit/VOC2012/JPEGImages/2007_005210.jpg')

x = np.array(x)

plt.imshow(x)

print(x.shape)

(281, 500, 3)

x = torch.from_numpy(x.astype('float32')).permute(2, 0, 1).unsqueeze(0)

# 定义转置卷积

conv_trans = nn.ConvTranspose2d(3, 3, 4, 2, 1)

# 将其定义为 bilinear kernel

conv_trans.weight.data = bilinear_kernel(3, 3, 4)

y = conv_trans(Variable(x)).data.squeeze().permute(1, 2, 0).numpy()

plt.imshow(y.astype('uint8'))

print(y.shape)

(562, 1000, 3)

可以看到通过双线性的 kernel 进行转置卷积,图片的大小扩大了一倍,但是图片看上去仍然非常的清楚,所以这种方式的上采样具有很好的效果

下面我们使用 resnet 34 代替论文中的 vgg 实现 fcn

# 使用预训练的 resnet 34

pretrained_net = model_zoo.resnet34(pretrained=True)

num_classes = len(classes)

这里我们去掉最后的 avgpool 和 fc 层,使用 list(pretrained_net.children())[:-2] 就能够取到倒数第三层

下面我们开始定义我们的网络结构,就像上面显示的一样,我们会取出最后的三个结果进行合并

class fcn(nn.Module):

def __init__(self, num_classes):

super(fcn, self).__init__()

self.stage1 = nn.Sequential(*list(pretrained_net.children())[:-4]) # 第一段

self.stage2 = list(pretrained_net.children())[-4] # 第二段

self.stage3 = list(pretrained_net.children())[-3] # 第三段

self.scores1 = nn.Conv2d(512, num_classes, 1)

self.scores2 = nn.Conv2d(256, num_classes, 1)

self.scores3 = nn.Conv2d(128, num_classes, 1)

self.upsample_8x = nn.ConvTranspose2d(num_classes, num_classes, 16, 8, 4, bias=False)

self.upsample_8x.weight.data = bilinear_kernel(num_classes, num_classes, 16) # 使用双线性 kernel

self.upsample_4x = nn.ConvTranspose2d(num_classes, num_classes, 4, 2, 1, bias=False)

self.upsample_4x.weight.data = bilinear_kernel(num_classes, num_classes, 4) # 使用双线性 kernel

self.upsample_2x = nn.ConvTranspose2d(num_classes, num_classes, 4, 2, 1, bias=False)

self.upsample_2x.weight.data = bilinear_kernel(num_classes, num_classes, 4) # 使用双线性 kernel

def forward(self, x):

x = self.stage1(x)

s1 = x # 1/8

x = self.stage2(x)

s2 = x # 1/16

x = self.stage3(x)

s3 = x # 1/32

s3 = self.scores1(s3)

s3 = self.upsample_2x(s3)

s2 = self.scores2(s2)

s2 = s2 + s3

s1 = self.scores3(s1)

s2 = self.upsample_4x(s2)

s = s1 + s2

s = self.upsample_8x(s2)

return s

接着我们定义一些语义分割常用的指标,比如 overal accuracy,mean IU 等等,下面这个是参考 wkentaro 的 pytorch-fcn 得到的

def _fast_hist(label_true, label_pred, n_class):

mask = (label_true >= 0) & (label_true < n_class)

hist = np.bincount(

n_class * label_true[mask].astype(int) +

label_pred[mask], minlength=n_class ** 2).reshape(n_class, n_class)

return hist

def label_accuracy_score(label_trues, label_preds, n_class):

"""Returns accuracy score evaluation result.- overall accuracy- mean accuracy- mean IU- fwavacc"""

hist = np.zeros((n_class, n_class))

for lt, lp in zip(label_trues, label_preds):

hist += _fast_hist(lt.flatten(), lp.flatten(), n_class)

acc = np.diag(hist).sum() / hist.sum()

acc_cls = np.diag(hist) / hist.sum(axis=1)

acc_cls = np.nanmean(acc_cls)

iu = np.diag(hist) / (hist.sum(axis=1) + hist.sum(axis=0) - np.diag(hist))

mean_iu = np.nanmean(iu)

freq = hist.sum(axis=1) / hist.sum()

fwavacc = (freq[freq > 0] * iu[freq > 0]).sum()

return acc, acc_cls, mean_iu, fwavacc

然后我们定义 loss,这里使用 torch.nn.NLLLoss2d(),其实这和交叉熵一样,都是处理分类问题的 loss,只是这个 loss 需要在最后一层输出加上 torch.nn.functional.log_softmax,但是这个 loss 可以作用在 2d 的输出上,于是我们就选择这个 loss

# 定义 loss 和 optimizer

from mxtorch.trainer import ScheduledOptim

criterion = nn.NLLLoss2d()

basic_optim = torch.optim.SGD(net.parameters(), lr=1e-2, weight_decay=1e-4)

optimizer = ScheduledOptim(basic_optim)

训练

for e in range(80):

if e > 0 and e % 50 == 0:

optimizer.set_learning_rate(optimizer.learning_rate * 0.1)

train_loss = 0

train_acc = 0

train_acc_cls = 0

train_mean_iu = 0

train_fwavacc = 0

prev_time = datetime.now()

net = net.train()

for data in train_data:

im = Variable(data[0].cuda())

label = Variable(data[1].cuda())

# forward

out = net(im)

out = F.log_softmax(out, dim=1) # (b, n, h, w)

loss = criterion(out, label)

# backward

optimizer.zero_grad()

loss.backward()

optimizer.step()

train_loss += loss.data[0]

label_pred = out.max(dim=1)[1].data.cpu().numpy()

label_true = label.data.cpu().numpy()

for lbt, lbp in zip(label_true, label_pred):

acc, acc_cls, mean_iu, fwavacc = label_accuracy_score(lbt, lbp, num_classes)

train_acc += acc

train_acc_cls += acc_cls

train_mean_iu += mean_iu

train_fwavacc += fwavacc

net = net.eval()

eval_loss = 0

eval_acc = 0

eval_acc_cls = 0

eval_mean_iu = 0

eval_fwavacc = 0

for data in valid_data:

im = Variable(data[0].cuda(), volatile=True)

label = Variable(data[1].cuda(), volatile=True)

# forward

out = net(im)

out = F.log_softmax(out, dim=1)

loss = criterion(out, label)

eval_loss += loss.data[0]

label_pred = out.max(dim=1)[1].data.cpu().numpy()

label_true = label.data.cpu().numpy()

for lbt, lbp in zip(label_true, label_pred):

acc, acc_cls, mean_iu, fwavacc = label_accuracy_score(lbt, lbp, num_classes)

eval_acc += acc

eval_acc_cls += acc_cls

eval_mean_iu += mean_iu

eval_fwavacc += fwavacc

cur_time = datetime.now()

h, remainder = divmod((cur_time - prev_time).seconds, 3600)

m, s = divmod(remainder, 60)

epoch_str = ('Epoch:{}, Train Loss:{:.5f}, Train Acc:{:.5f}, Train Mean IU:{:.5f},\Valid Loss:{:.5f}, Valid Acc:{:.5f}, Valid Mean IU:{:.5f}'.format(

e, train_loss / len(train_data), train_acc / len(voc_train), train_mean_iu / len(voc_train),

eval_loss / len(valid_data), eval_acc / len(voc_test), eval_mean_iu / len(voc_test)))

time_str = 'Time:{:.0f}:{:.0f}:{:.0f}'.format(h, m, s)

print(epoch_str + time_str + ' lr:{}'.format(optimizer.learning_rate))

Epoch: 77, Train Loss: 0.23017, Train Acc: 0.92420, Train Mean IU: 0.64684, Valid Loss: 0.39680, Valid Acc: 0.87509, Valid Mean IU: 0.53005 Time: 0:0:41 lr: 0.001

Epoch: 78, Train Loss: 0.23212, Train Acc: 0.92334, Train Mean IU: 0.64237, Valid Loss: 0.39651, Valid Acc: 0.87538, Valid Mean IU: 0.52885 Time: 0:0:41 lr: 0.001

Epoch: 79, Train Loss: 0.23251, Train Acc: 0.92364, Train Mean IU: 0.64002, Valid Loss: 0.39242, Valid Acc: 0.87575, Valid Mean IU: 0.53311 Time: 0:0:41 lr: 0.001

可以看到,我们的模型在训练集上的 mean IU 达到了 0.64 左右,验证集上的 mean IU 达到了 0.53 左右,下面我们可视化一下最后的结果

# 定义预测函数

cm = np.array(colormap).astype('uint8')

def predict(im, label): # 预测结果

im = Variable(im.unsqueeze(0)).cuda()

out = net(im)

pred = out.max(1)[1].squeeze().cpu().data.numpy()

pred = cm[pred]

return pred, cm[label.numpy()]

_, figs = plt.subplots(6, 3, figsize=(12, 10))

for i in range(6):

test_data, test_label = voc_test[i]

pred, label = predict(test_data, test_label)

figs[i, 0].imshow(Image.open(voc_test.data_list[i]))

figs[i, 0].axes.get_xaxis().set_visible(False)

figs[i, 0].axes.get_yaxis().set_visible(False)

figs[i, 1].imshow(label)

figs[i, 1].axes.get_xaxis().set_visible(False)

figs[i, 1].axes.get_yaxis().set_visible(False)

figs[i, 2].imshow(pred)

figs[i, 2].axes.get_xaxis().set_visible(False)

figs[i, 2].axes.get_yaxis().set_visible(False)

可以看到,通过训练,模型已经有了基本效果,但是离论文中的效果有差距,有可能是训练的问题,更多的原因应该是没有使用 caffe model 中的 vgg 模型,这里特别提醒一下,如果要使用 caffe model,那么图像预处理的时候要是 BRG 的格式,同时大小是 0 ~ 255,减去均值。

另外代码中使用的 mxtorch 可以在我的 github 主页看到

本文的代码

欢迎查看我的知乎专栏 深度炼丹

欢迎访问我的博客

基于pytorch的fcn_FCN 的简单实现相关推荐

  1. 基于pytorch的FCN网络简单实现

    参考知乎专栏实现FCN网络https://zhuanlan.zhihu.com/p/32506912 import torch from torch import nn import torch.nn ...

  2. 基于PyTorch,如何构建一个简单的神经网络

    本文为 PyTorch 官方教程中:如何构建神经网络.基于 PyTorch 专门构建神经网络的子模块 torch.nn 构建一个简单的神经网络. 完整教程运行 codelab→ https://ope ...

  3. 【项目实战课】人人免费可学!基于Pytorch的图像分类简单任务数据增强实战

    欢迎大家来到我们的项目实战课,本期内容是<基于Pytorch的图像分类简单任务数据增强实战>.所谓项目实战课,就是以简单的原理回顾+详细的项目实战的模式,针对具体的某一个主题,进行代码级的 ...

  4. 基于Pytorch的简单深度学习项目实战

    目录 任务详情 训练设置 网络模型 损失函数 优化器 训练步骤 具体代码 导入环境 导入数据 加载数据 创建网络 损失函数 优化器 开始训练 任务详情 利用CIFAR10数据集,基于pytorch环境 ...

  5. 基于pytorch后量化(mnist分类)---浮点训练vs多bit后量化vs多bit量化感知训练效果对比

    基于pytorch后量化(mnist分类)-浮点训练vs多bit后量化vs多bit量化感知训练效果对比 代码下载地址:下载地址 试了 bit 数为 1-8 的准确率,得到下面这张折线图: 发现,当 b ...

  6. 艾伦人工智能研究院开源AllenNLP,基于PyTorch轻松构建NLP模型

    艾伦人工智能研究院(AI2)开源AllenNLP,它是一个基于PyTorch的NLP研究库,利用深度学习来进行自然语言理解,通过处理低层次的细节.提供高质量的参考实现,能轻松快速地帮助研究员构建新的语 ...

  7. YOLOv5 它来了!基于 PyTorch,体积比 YOLOv4 小 90%,速度却超 2 倍!

    贾浩楠 发自 凹非寺 量子位 报道 | 公众号 QbitAI YOLOv4还没有退热,YOLOv5已经发布! 6月9日,Ultralytics公司开源了YOLOv5,离上一次YOLOv4发布不到50天 ...

  8. 【深度学习】梯度和方向导数概念解析(代码基于Pytorch实现)

    [深度学习]梯度和方向导数概念解析(代码基于Pytorch实现) 文章目录 1 方向导数 2 梯度 3 自动求导实现 4 梯度下降4.1 概述4.2 小批量梯度下降 5 总结 1 方向导数 方向导数的 ...

  9. 【项目实战课】基于Pytorch的SRGAN图像超分辨实战

    欢迎大家来到我们的项目实战课,本期内容是<基于Pytorch的SRGAN图像超分辨实战>.所谓项目实战课,就是以简单的原理回顾+详细的项目实战的模式,针对具体的某一个主题,进行代码级的实战 ...

最新文章

  1. HTML中的转义字符 (转)
  2. Hadoop中Writable和WritableComparable区别
  3. t1plus 用什么服务器系统,T1 Plus商贸宝普及版与用友T1系统哪个更好呢?
  4. 1.2 正交化-深度学习第三课《结构化机器学习项目》-Stanford吴恩达教授
  5. 外网数据同步到内网方案_数据同步之解决方案
  6. java in think 多态问题
  7. Moment.js在Vue中使用
  8. 【深度优先搜索】20行代码解决8皇后问题
  9. 索尼高清影视技术学院参观观后感
  10. 使用diff和patch制作及打补丁
  11. Nginx: 解决connect() to xxxx failed (13: Permission denied) while connecting to upstream的问题
  12. android 蓝牙信息提醒,Android蓝牙与BLE通信相关的读、写、通知、指示操作
  13. 计步器锁屏后依然工作的原理
  14. excel函数获取长域名的顶级域名
  15. 手机如何连接无线打印服务器,手机如何设置打印服务器的无线WiFi参数?(适用于 B1版)...
  16. 怎样做才能得到自己想要的
  17. 如何把现有的流程系统数据迁徙到驰骋BPM上?
  18. python绘制混淆矩阵(2s-AGCN结果分析)
  19. Netcraft brain-dead
  20. Swift Jama Matrix线性代数矩阵运算库

热门文章

  1. iOS iBeacon 使用
  2. U盘移动硬盘变本地硬盘怎么办 ,移动硬盘变本地硬盘的恢复方法
  3. ChatGPT进阶系列-如何使用 ChatGPT
  4. 基于RFID技术的电力计能表仓储管理系统—铨顺宏
  5. 30多岁程序员哭诉:得了抑郁症,请假在家躺尸数周,一点都没有好转。现在不敢辞职,工作又做不下去,每天想自杀!...
  6. iPad2 iOS 5.0.1+ Xcode 4.3 免证书(iDP)开发 + 真机调试 生成IPA全攻略
  7. minicom指令_Minicom 使用初步
  8. kfold_关于sklearn.model_selected中的KFold.split的理解
  9. JavaScript 的黑暗时刻来了?
  10. 伯禹 动手学深度学习 打卡12之 RNN 进阶