本章代码:

这篇文章主要介绍了 PyTorch 中的优化器,包括 3 个部分:优化器的概念、optimizer 的属性、optimizer 的方法。

TensorBoard 是 TensorFlow 中强大的可视化工具,支持标量、文本、图像、音频、视频和 Embedding 等多种数据可视化。

在 PyTorch 中也可以使用 TensorBoard,具体是使用 TensorboardX 来调用 TensorBoard。除了安装 TensorboardX,还要安装 TensorFlow 和 TensorBoard,其中 TensorFlow 和 TensorBoard 需要一致。

TensorBoardX 可视化的流程需要首先编写 Python 代码把需要可视化的数据保存到 event file 文件中,然后再使用 TensorBoardX 读取 event file 展示到网页中。

下面的代码是一个保存 event file 的例子:

    import numpy as np
import matplotlib.pyplot as plt
from tensorboardX import SummaryWriter
from common_tools import set_seed
max_epoch = 100 writer = SummaryWriter(comment='test_comment', filename_suffix="test_suffix") for x in range(max_epoch): writer.add_scalar('y=2x', x * 2, x)
writer.add_scalar('y=pow_2_x', 2 ** x, x) writer.add_scalars('data/scalar_group', {"xsinx": x * np.sin(x),
"xcosx": x * np.cos(x)}, x) writer.close()

上面具体保存的数据,我们先不关注,主要关注的是保存 event file 需要用到 SummaryWriter 类,这个类是用于保存数据的最重要的类,执行完后,会在当前文件夹生成一个runs的文件夹,里面保存的就是数据的 event file。

然后在命令行中输入tensorboard --logdir=lesson5/runs启动 tensorboard 服务,其中lesson5/runsruns文件夹的路径。然后命令行会显示 tensorboard 的访问地址:

TensorBoard 1.9.0 at http://LAPTOP-DPDNNJSU:6006 (Press CTRL+C to quit)

在浏览器中打开,显示如下:

最上面的一栏显示的是数据类型,由于我们在代码中只记录了 scalar 类型的数据,因此只显示`SCALARS`。

右上角有一些功能设置

点击`INACTIVE`显示我们没有记录的数据类型。设置里可以设置刷新 tensorboard 的间隔,在模型训练时可以实时监控数据的变化。

左边的菜单栏如下,点击Show data download links可以展示每个图的下载按钮,如果一个图中有多个数据,需要选中需要下载的曲线,然后下载,格式有 csvjson可选。

第二个选项`Ignore outliers in chart scaling`可以设置是否忽略离群点,在`y_pow_2_x`中,数据的尺度达到了 $10^{18}$,勾选`Ignore outliers in chart scaling`后 $y$ 轴的尺度下降到 $10^{17}$。

Soothing 是对图像进行平滑,下图中,颜色较淡的阴影部分才是真正的曲线数据,Smoothing 设置为了 0.6,进行了平滑才展示为颜色较深的线。

Smoothing 设置为 0,没有进行平滑,显示如下:

Smoothing 设置为 1,则平滑后的线和 $x$ 轴重合,显示如下:

`Horizontal Axis`表示横轴:`STEP`表示原始数据作为横轴,`RELATIVE`和`WALL`都是以时间作为横轴,单位是小时,`RELATIVE`是相对时间,`WALL`是绝对时间。

runs显示所有的 event file,可以选择展示某些 event file 的图像,其中正方形按钮是多选,圆形按钮是单选。

上面的搜索框可以根据 tags 来搜索数据对应的图像

# optimizer 的属性

PyTorch 中提供了 Optimizer 类,定义如下:

class Optimizer(object):
def __init__(self, params, defaults):
self.defaults = defaults
self.state = defaultdict(dict)
self.param_groups = []

主要有 3 个属性

  • defaults:优化器的超参数,如 weight_decay,momentum
  • state:参数的缓存,如 momentum 中需要用到前几次的梯度,就缓存在这个变量中
  • param_groups:管理的参数组,是一个 list,其中每个元素是字典,包括 momentum、lr、weight_decay、params 等。
  • _step_count:记录更新 次数,在学习率调整中使用

SummaryWriter

torch.utils.tensorboard.writer.SummaryWriter(log_dir=None, comment='', purge_step=None, max_queue=10, flush_secs=120, filename_suffix='')

功能:提供创建 event file 的高级接口

主要功能:

  • log_dir:event file 输出文件夹,默认为runs文件夹
  • comment:不指定 log_dir 时,runs文件夹里的子文件夹后缀
  • filename_suffix:event_file 文件名后缀

代码如下:

    log_dir = "./train_log/test_log_dir"
writer = SummaryWriter(log_dir=log_dir, comment='_scalars', filename_suffix="12345678")
# writer = SummaryWriter(comment='_scalars', filename_suffix="12345678") for x in range(100):
writer.add_scalar('y=pow_2_x', 2 ** x, x) writer.close()

运行后会生成train_log/test_log_dir文件夹,里面的 event file 文件名后缀是12345678

但是我们指定了`log_dir`,`comment`参数没有生效。如果想要`comment`参数生效,把`SummaryWriter`的初始化改为`writer = SummaryWriter(comment='_scalars', filename_suffix="12345678")`,生成的文件夹如下,`runs`里的子文件夹后缀是`_scalars`。

# add_scalar

add_scalar(tag, scalar_value, global_step=None, walltime=None)

功能:记录标量

  • tag:图像的标签名,图的唯一标识
  • scalar_value:要记录的标量,y 轴的数据
  • global_step:x 轴的数据

add_scalars

上面的add_scalar()只能记录一条曲线的数据。但是我们在实际中可能需要在一张图中同时展示多条曲线,比如在训练模型时,经常需要同时查看训练集和测试集的 loss。这时我们可以使用add_scalars()方法

add_scalars(main_tag, tag_scalar_dict, global_step=None, walltime=None)
  • main_tag:该图的标签
  • tag_scalar_dict:用字典的形式记录多个曲线。key 是变量的 tag,value 是变量的值

代码如下:

    max_epoch = 100
writer = SummaryWriter(comment='test_comment', filename_suffix="test_suffix")
for x in range(max_epoch):
writer.add_scalar('y=2x', x * 2, x)
writer.add_scalar('y=pow_2_x', 2 ** x, x)
writer.add_scalars('data/scalar_group', {"xsinx": x * np.sin(x),
"xcosx": x * np.cos(x)}, x)
writer.close()

运行后生成 event file,然后使用 TensorBoard 来查看如下:

每个图像下面都有 3 个按钮,中间的按钮是以对数形式展示 y 轴。如对`y=pow_2_x`曲线的 y 轴取对数展示如下,变成了直线。

# add_histogram

add_histogram(tag, values, global_step=None, bins='tensorflow', walltime=None, max_bins=None)

功能:统计直方图与多分位折线图

  • tag:图像的标签名,图的唯一标识
  • values:要统计的参数,通常统计权值、偏置或者梯度
  • global_step:第几个子图
  • bins:取直方图的 bins

下面的代码构造了均匀分布和正态分布,循环生成了 2 次,分别用matplotlib和 TensorBoard 进行画图。

    writer = SummaryWriter(comment='test_comment', filename_suffix="test_suffix")
for x in range(2):
np.random.seed(x)
data_union = np.arange(100)
data_normal = np.random.normal(size=1000)
writer.add_histogram('distribution union', data_union, x)
writer.add_histogram('distribution normal', data_normal, x)
plt.subplot(121).hist(data_union, label="union")
plt.subplot(122).hist(data_normal, label="normal")
plt.legend()
plt.show()
writer.close()

matplotlib画图显示如下:

TensorBoard 显示结果如下。

正态分布显示如下,每个子图分别对应一个 global_step:

均匀分布显示如下,显示曲线的原因和`bins`参数设置有关,默认是`tensorflow`:

除此之外,还会得到`DISTRIBUTIONS`,这是多分位折线图,纵轴有 9 个折线,表示数据的分布区间,某个区间的颜色越深,表示这个区间的数所占比例越大。横轴是 global_step。这个图的作用是观察数方差的变化情况。显示如下:

# 模型指标监控

下面使用 TensorBoard 来监控人民币二分类实验训练过程中的 loss、accuracy、weights 和 gradients 的变化情况。

首先定义一个SummaryWriter

writer = SummaryWriter(comment='test_your_comment', filename_suffix="_test_your_filename_suffix")

然后在每次训练中记录 loss 和 accuracy 的值

# 记录数据,保存于event file
writer.add_scalars("Loss", {"Train": loss.item()}, iter_count)
writer.add_scalars("Accuracy", {"Train": correct / total}, iter_count)

并且在验证时记录所有验证集样本的 loss 和 accuracy 的均值

# 记录数据,保存于event file
writer.add_scalars("Loss", {"Valid": np.mean(valid_curve)}, iter_count)
writer.add_scalars("Accuracy", {"Valid": correct / total}, iter_count)

并且在每个 epoch 中记录每一层权值以及权值的梯度。

    # 每个epoch,记录梯度,权值
for name, param in net.named_parameters():
writer.add_histogram(name + '_grad', param.grad, epoch)
writer.add_histogram(name + '_data', param, epoch)

在训练还没结束时,就可以启动 TensorBoard 可视化,Accuracy 的可视化如下,颜色较深的是训练集的 Accuracy,颜色较浅的是 验证集的样本:

Loss 的可视化如下,其中验证集的 Loss 是从第 10 个 epoch 才开始记录的,并且 验证集的 Loss 是所有验证集样本的 Loss 均值,所以曲线更加平滑;而训练集的 Loss 是 batch size 的数据,因此震荡幅度较大:

上面的 Loss 曲线图与使用`matplotlib`画的图不太一样,因为 TensorBoard 默认会进行 Smoothing,我们把 Smoothing 系数设置为 0 后,显示如下:

而记录权值以及权值梯度的 HISTOGRAMS 显示如下,记录了每一层的数据:

展开查看第一层的权值和梯度。

可以看到每一个 epoch 的梯度都是呈正态分布,说明权值分布比较好;梯度都是接近于 0,说明模型很快就收敛了。通常我们使用 TensorBoard 查看我们的网络参数在训练时的分布变化情况,如果分布很奇怪,并且 Loss 没有下降,这时需要考虑是什么原因改变了数据的分布较大的。如果前面网络层的梯度很小,后面网络层的梯度比较大,那么可能是梯度消失,因为后面网络层的较大梯度反向传播到前面网络层时已经变小了。如果前后网络层的梯度都很小,那么说明不是梯度消失,而是因为 Loss 很小,模型已经接近收敛。

add_image

add_image(tag, img_tensor, global_step=None, walltime=None, dataformats='CHW')

功能:记录图像

  • tag:图像的标签名,图像的唯一标识
  • img_tensor:图像数据,需要注意尺度
  • global_step:记录这是第几个子图
  • dataformats:数据形式,取值有'CHW','HWC','HW'。如果像素值在 [0, 1] 之间,那么默认会乘以 255,放大到 [0, 255] 范围之间。如果有大于 1 的像素值,认为已经是 [0, 255] 范围,那么就不会放大。

代码如下:

writer = SummaryWriter(comment='test_your_comment', filename_suffix="_test_your_filename_suffix")

# img 1     random
# 随机噪声的图片
fake_img = torch.randn(3, 512, 512)
writer.add_image("fake_img", fake_img, 1)
time.sleep(1) # img 2 ones
# 像素值全为 1 的图片,会乘以 255,所以是白色的图片
fake_img = torch.ones(3, 512, 512)
time.sleep(1)
writer.add_image("fake_img", fake_img, 2) # img 3 1.1
# 像素值全为 1.1 的图片,不会乘以 255,所以是黑色的图片
fake_img = torch.ones(3, 512, 512) * 1.1
time.sleep(1)
writer.add_image("fake_img", fake_img, 3) # img 4 HW
fake_img = torch.rand(512, 512)
writer.add_image("fake_img", fake_img, 4, dataformats="HW") # img 5 HWC
fake_img = torch.rand(512, 512, 3)
writer.add_image("fake_img", fake_img, 5, dataformats="HWC") writer.close()

使用 TensorBoard 可视化如下:

图片上面的`step`可以选择第几张图片,如选择第 3 张图片,显示如下:

# torchvision.utils.make_grid

上面虽然可以通过拖动显示每张图片,但实际中我们希望在网格中同时展示多张图片,可以用到make_grid()函数。

torchvision.utils.make_grid(tensor: Union[torch.Tensor, List[torch.Tensor]], nrow: int = 8, padding: int = 2, normalize: bool = False, range: Optional[Tuple[int, int]] = None, scale_each: bool = False, pad_value: int = 0)

功能:制作网格图像

  • tensor:图像数据,$B \times C \times H \times W$ 的形状
  • nrow:行数(列数是自动计算的,为:$\frac{B}{nrow}$)
  • padding:图像间距,单位是像素,默认为 2
  • normalize:是否将像素值标准化到 [0, 255] 之间
  • range:标准化范围,例如原图的像素值范围是 [-1000, 2000],设置 range 为 [-600, 500],那么会把小于 -600 的像素值变为 -600,那么会把大于 500 的像素值变为 500,然后标准化到 [0, 255] 之间
  • scale_each:是否单张图维度标准化
  • pad_value:间隔的像素值

下面的代码是人民币图片的网络可视化,batch_size 设置为 16,nrow 设置为 4,得到 4 行 4 列的网络图像

writer = SummaryWriter(comment='test_your_comment', filename_suffix="_test_your_filename_suffix")

split_dir = os.path.join(enviroments.project_dir, "data", "rmb_split")
train_dir = os.path.join(split_dir, "train")
# train_dir = "path to your training data"
# 先把宽高缩放到 [32, 64] 之间,然后使用 toTensor 把 Image 转化为 tensor,并把像素值缩放到 [0, 1] 之间
transform_compose = transforms.Compose([transforms.Resize((32, 64)), transforms.ToTensor()])
train_data = RMBDataset(data_dir=train_dir, transform=transform_compose)
train_loader = DataLoader(dataset=train_data, batch_size=16, shuffle=True)
data_batch, label_batch = next(iter(train_loader)) img_grid = vutils.make_grid(data_batch, nrow=4, normalize=True, scale_each=True)
# img_grid = vutils.make_grid(data_batch, nrow=4, normalize=False, scale_each=False)
writer.add_image("input img", img_grid, 0) writer.close()

TensorBoard 显示如下:

# AlexNet 卷积核与特征图可视化

使用 TensorBoard 可视化 AlexNet 网络的前两层卷积核。其中每一层的卷积核都把输出的维度作为 global_step,包括两种可视化方式:一种是每个 (w, h) 维度作为灰度图,添加一个 c 的维度,形成 (b, c, h, w),其中 b 是 输入的维度;另一种是把整个卷积核 reshape 到 c 是 3 的形状,再进行可视化。详细见如下代码:

    writer = SummaryWriter(comment='test_your_comment', filename_suffix="_test_your_filename_suffix")

    alexnet = models.alexnet(pretrained=True)

    # 当前遍历到第几层网络的卷积核了
kernel_num = -1
# 最多显示两层网络的卷积核:第 0 层和第 1 层
vis_max = 1 # 获取网络的每一层
for sub_module in alexnet.modules():
# 判断这一层是否为 2 维卷积层
if isinstance(sub_module, nn.Conv2d):
kernel_num += 1
# 如果当前层大于1,则停止记录权值
if kernel_num > vis_max:
break
# 获取这一层的权值
kernels = sub_module.weight
# 权值的形状是 [c_out, c_int, k_w, k_h]
c_out, c_int, k_w, k_h = tuple(kernels.shape) # 根据输出的每个维度进行可视化
for o_idx in range(c_out):
# 取出的数据形状是 (c_int, k_w, k_h),对应 BHW; 需要扩展为 (c_int, 1, k_w, k_h),对应 BCHW
kernel_idx = kernels[o_idx, :, :, :].unsqueeze(1) # make_grid需要 BCHW,这里拓展C维度
# 注意 nrow 设置为 c_int,所以行数为 1。在 for 循环中每 添加一个,就会多一个 global_step
kernel_grid = vutils.make_grid(kernel_idx, normalize=True, scale_each=True, nrow=c_int)
writer.add_image('{}_Convlayer_split_in_channel'.format(kernel_num), kernel_grid, global_step=o_idx)
# 因为 channe 为 3 时才能进行可视化,所以这里 reshape
kernel_all = kernels.view(-1, 3, k_h, k_w) #b, 3, h, w
kernel_grid = vutils.make_grid(kernel_all, normalize=True, scale_each=True, nrow=8) # c, h, w
writer.add_image('{}_all'.format(kernel_num), kernel_grid, global_step=kernel_num+1) print("{}_convlayer shape:{}".format(kernel_num, tuple(kernels.shape))) writer.close()

使用 TensorBoard 可视化如下。

这是根据输出的维度分批展示第一层卷积核的可视化

这是根据输出的维度分批展示第二层卷积核的可视化

这是整个第一层卷积核的可视化

这是整个第二层卷积核的可视化

下面把 AlexNet 的第一个卷积层的输出进行可视化,首先对图片数据进行预处理(resize,标准化等操作)。由于在定义模型时,网络层通过nn.Sequential() 堆叠,保存在 features 变量中。因此通过 features 获取第一个卷积层。把图片输入卷积层得到输出,形状为 (1, 64, 55, 55),需要转换为 (64, 1, 55, 55),对应 (B, C, H, W),nrow 设置为 8,最后进行可视化,代码如下:

    writer = SummaryWriter(comment='test_your_comment', filename_suffix="_test_your_filename_suffix")

    # 数据
path_img = "./lena.png" # your path to image
normMean = [0.49139968, 0.48215827, 0.44653124]
normStd = [0.24703233, 0.24348505, 0.26158768] norm_transform = transforms.Normalize(normMean, normStd)
img_transforms = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
norm_transform
]) img_pil = Image.open(path_img).convert('RGB')
if img_transforms is not None:
img_tensor = img_transforms(img_pil)
img_tensor.unsqueeze_(0) # chw --> bchw # 模型
alexnet = models.alexnet(pretrained=True) # forward
# 由于在定义模型时,网络层通过nn.Sequential() 堆叠,保存在 features 变量中。因此通过 features 获取第一个卷积层
convlayer1 = alexnet.features[0]
# 把图片输入第一个卷积层
fmap_1 = convlayer1(img_tensor) # 预处理
fmap_1.transpose_(0, 1) # bchw=(1, 64, 55, 55) --> (64, 1, 55, 55)
fmap_1_grid = vutils.make_grid(fmap_1, normalize=True, scale_each=True, nrow=8) writer.add_image('feature map in conv1', fmap_1_grid, global_step=322)
writer.close()

使用 TensorBoard 可视化如下:

# add_graph

add_graph(model, input_to_model=None, verbose=False)

功能:可视化模型计算图

  • model:模型,必须继承自 nn.Module
  • input_to_model:输入给模型的数据,形状为 BCHW
  • verbose:是否打印图结构信息

查看 LeNet 的计算图代码如下:

    writer = SummaryWriter(comment='test_your_comment', filename_suffix="_test_your_filename_suffix")

    # 模型
fake_img = torch.randn(1, 3, 32, 32)
lenet = LeNet(classes=2)
writer.add_graph(lenet, fake_img)
writer.close()

使用 TensorBoard 可视化如下:

torchsummary

模型计算图的可视化还是比较复杂,不够清晰。而torchsummary能够查看模型的输入和输出的形状,可以更加清楚地输出模型的结构。

torchsummary.summary(model, input_size, batch_size=-1, device="cuda")

功能:查看模型的信息,便于调试

  • model:pytorch 模型,必须继承自 nn.Module
  • input_size:模型输入 size,形状为 CHW
  • batch_size:batch_size,默认为 -1,在展示模型每层输出的形状时显示的 batch_size
  • device:"cuda"或者"cpu"

查看 LeNet 的模型信息代码如下:

    # 模型
lenet = LeNet(classes=2)
print(summary(lenet, (3, 32, 32), device="cpu"))

输出如下:

----------------------------------------------------------------
Layer (type) Output Shape Param #
================================================================
Conv2d-1 [-1, 6, 28, 28] 456
Conv2d-2 [-1, 16, 10, 10] 2,416
Linear-3 [-1, 120] 48,120
Linear-4 [-1, 84] 10,164
Linear-5 [-1, 2] 170
================================================================
Total params: 61,326
Trainable params: 61,326
Non-trainable params: 0
----------------------------------------------------------------
Input size (MB): 0.01
Forward/backward pass size (MB): 0.05
Params size (MB): 0.23
Estimated Total Size (MB): 0.30
----------------------------------------------------------------
None

上述信息分别有模型每层的输出形状,每层的参数数量,总的参数数量,以及模型大小等信息。

我们以第一层为例,第一层卷积核大小是 (6, 3, 5, 5),每个卷积核还有一个偏置,因此 $6 \times 3 \times 5 \times 5+6=456$。

参考资料

如果你觉得这篇文章对你有帮助,不妨点个赞,让我有更多动力写出好文章。

[PyTorch 学习笔记] 5.1 TensorBoard 介绍的更多相关文章

  1. Python学习笔记—Python基础1 介绍、发展史、安装、基本语法

    第一周学习笔记: 一.Python介绍      1.Python的创始人为吉多·范罗苏姆.1989年的圣诞节期间,吉多·范罗苏姆为了在阿姆斯特丹打发时间,决心开发一个新的脚本解释程序,作为ABC语言 ...

  2. 《精通并发与Netty》学习笔记(01 - netty介绍及环境搭建)

    一.Netty介绍     Netty是由JBOSS提供的一个java开源框架.Netty提供异步的.事件驱动的网络应用程序框架和工具,用以快速开发高性能.高可靠性的网络服务器和客户端程序.     ...

  3. [原创]java WEB学习笔记43:jstl 介绍,core库详解:表达式操作,流程控制,迭代操作,url操作

    本博客为原创:综合 尚硅谷(http://www.atguigu.com)的系统教程(深表感谢)和 网络上的现有资源(博客,文档,图书等),资源的出处我会标明 本博客的目的:①总结自己的学习过程,相当 ...

  4. bootstrap 学习笔记(1)---介绍bootstrap和栅格系统

    学习前端许久,对于布置框架和响应浏览器用html 和javascript 写的有点繁琐,无意间看到这个框架,觉得挺好用的就开始学习了,但是这个框架上面有很多知识,不是所有的都要学的,故将学习笔记和觉得 ...

  5. 微信小程序学习笔记一 小程序介绍 & 前置知识

    微信小程序学习笔记一 1. 什么是小程序? 2017年度百度百科十大热词之一 微信小程序, 简称小程序, 英文名 Mini Program, 是一种不需要下载安装即可使用的应用 ( 张小龙对其的定义是 ...

  6. pytorch学习笔记(九):PyTorch结构介绍

    PyTorch结构介绍对PyTorch架构的粗浅理解,不能保证完全正确,但是希望可以从更高层次上对PyTorch上有个整体把握.水平有限,如有错误,欢迎指错,谢谢! 几个重要的类型和数值相关的Tens ...

  7. [PyTorch 学习笔记] 1.2 Tensor(张量)介绍

    本章代码: https://github.com/zhangxiann/PyTorch_Practice/blob/master/lesson1/tensor_introduce1.py https: ...

  8. Tensorflow学习笔记3:TensorBoard可视化学习

    TensorBoard简介 Tensorflow发布包中提供了TensorBoard,用于展示Tensorflow任务在计算过程中的Graph.定量指标图以及附加数据.大致的效果如下所示, Tenso ...

  9. [PyTorch 学习笔记] 1.3 张量操作与线性回归

    本章代码:https://github.com/zhangxiann/PyTorch_Practice/blob/master/lesson1/linear_regression.py 张量的操作 拼 ...

随机推荐

  1. sqlzoo刷题 SELECT from Nobel Tutorial

    SELECT from Nobel Tutorial 1.Change the query shown so that it displays Nobel prizes for 1950. SELEC ...

  2. Android 的重要控件 ListView (听说是最难最常用的控件)

    这个打字有点慢了,左手受伤了,不过很幸运,左手小拇指没事(这就可以愉快地使用快捷键啦!),虽然有伤,但还是得坚持总结,不只是为自己,还为未来的你们铺路,希望我写的,对你们有帮助. 提前给自己一个祝福: ...

  3. 一个简单的CPP处理框架

    好久没有在csdn上写过东西了,这么多年,一方面是工作忙,下班到家也没有开过电脑了,要陪小孩玩: 下面分享一段代码,是用CPP做的一个简单的消息(协议)处理框架: 是通过成员函数指针+map来实现的: ...

  4. 用Python玩连连看是什么效果?

    1.前言 Python实现的qq连连看辅助, 仅用于学习, 请在练习模式下使用, 请不要拿去伤害玩家们... 很多人学习python,不知道从何学起.很多人学习python,掌握了基本语法过后,不知道 ...

  5. CentOS7安装MinIO教程,并在C#客户端WPF中实现监控上传进度

    MinIO的详细介绍可以参考官网(https://min.io/product/overview). 简单来说它是一个实现了AWS S3标准的100%开源的,可商用的( Apache V2 licen ...

  6. 一个试图了解JVM内存模型的两年经验的初级程序员,透彻!

    所有的编程语言中都有内存模型这个概念,区别于微架构的内存模型,高级语言的内存模型包括了编译器和微架构两部分.我试图了解了Java.C#和Go语言的内存模型,发现内容基本大同小异,只是这些语言在具体实现 ...

  7. 发布新版首页“外婆新家”升级版:全新的UI,熟悉的味道

    在7月30日我们我们忐忑不安地发布了新版网站首页,发布后迎接我们的不是新颜新风貌的惊喜,而是我们最担心的残酷现实——“让我们等这么多年,等来的就是这个新的丑容颜”,在大家的批评声中我们深深地认识到我们 ...

  8. Bytom 储蓄分红 DAPP 开发指南

    储蓄分红DAPP 储蓄分红合约简介 储蓄分红合约指的是项目方发起了一个锁仓计划(即储蓄合约和取现合约),用户可以在准备期自由选择锁仓金额参与该计划,等到锁仓到期之后还可以自动获取锁仓的利润.用户可以在 ...

  9. SCOI2020迷惑记

    睡了个好觉还是很困但没咋吃饭就出门了. 到了之后随便跟认得到的人扯了两句就进去了. 结果让我们站在外面等... 然后通知说不能自带水和吃的那我这个中午没吃饭的咋整啊. 马上啃了半块巧克力就进了考场,然 ...

  10. git使用-标签管理

    1.查看所有的标签 git tag 2.创建标签 git tag  name 3.指定提交标签的信息 git tag -a name -m "comment" 4.删除标签 git ...