写给程序员的机器学习入门 (四) - 训练过程中常用的技巧
写给程序员的机器学习入门 (四) - 训练过程中常用的技巧
这篇将会着重介绍使用 pytorch 进行机器学习训练过程中的一些常见技巧,掌握它们可以让你事半功倍。
使用的代码大部分会基于上一篇最后一个例子,即根据码农条件预测工资🙀,如果你没看上一篇请点击这里查看。
保存和读取模型状态
在 pytorch 中各种操作都是围绕 tensor 对象来的,模型的参数也是 tensor,如果我们把训练好的 tensor 保存到硬盘然后下次再从硬盘读取就可以直接使用了。
我们先来看看如何保存单个 tensor,以下代码运行在 python 的 REPL 中:
# 引用 pytorch
>>> import torch# 新建一个 tensor 对象
>>> a = torch.tensor([1, 2, 3], dtype=torch.float)# 保存 tensor 到文件 1.pt
>>> torch.save(a, "1.pt")# 从文件 1.pt 读取 tensor
>>> b = torch.load("1.pt")
>>> b
tensor([1., 2., 3.])
torch.save 保存 tensor 的时候会使用 python 的 pickle 格式,这个格式保证在不同的 python 版本间兼容,但不支持压缩内容,所以如果 tensor 非常大保存的文件将会占用很多空间,我们可以在保存前压缩,读取前解压缩以减少文件大小:
# 引用压缩库
>>> import gzip# 保存 tensor 到文件 1.pt,保存时使用 gzip 压缩
>>> torch.save(a, gzip.GzipFile("1.pt.gz", "wb"))# 从文件 1.pt 读取 tensor,读取时使用 gzip 解压缩
>>> b = torch.load(gzip.GzipFile("1.pt.gz", "rb"))
>>> b
tensor([1., 2., 3.])
torch.save 不仅支持保存单个 tensor 对象,还支持保存 tensor 列表或者词典 (实际上它还可以保存 tensor 以外的 python 对象,只要 pickle 格式支持),我们可以调用 state_dict
获取一个包含模型所有参数的集合,再用 torch.save 就可以保存模型的状态:
>>> from torch import nn
>>> class MyModel(nn.Module):
... def __init__(self):
... super().__init__()
... self.layer1 = nn.Linear(in_features=8, out_features=100)
... self.layer2 = nn.Linear(in_features=100, out_features=50)
... self.layer3 = nn.Linear(in_features=50, out_features=1)
... def forward(self, x):
... hidden1 = nn.functional.relu(self.layer1(x))
... hidden2 = nn.functional.relu(self.layer2(hidden1))
... y = self.layer3(hidden2)
... return y
...
>>> model = MyModel()
>>> model.state_dict()
OrderedDict([('layer1.weight', tensor([[ 0.2261, 0.2008, 0.0833, -0.2020, -0.0674, 0.2717, -0.0076, 0.1984],省略途中输出0.1347, 0.1356]])), ('layer3.bias', tensor([0.0769]))])>>> torch.save(model.state_dict(), gzip.GzipFile("model.pt.gz", "wb"))
读取模型状态可以使用 load_state_dict
函数,不过你需要保证模型的参数定义没有发生变化,否则读取会出错:
>>> new_model = MyModel()
>>> new_model.load_state_dict(torch.load(gzip.GzipFile("model.pt.gz", "rb")))
<All keys matched successfully>
一个很重要的细节是,如果你读取模型状态后不是准备继续训练,而是用于预测其他数据,那么你应该调用 eval
函数来禁止自动微分等功能,这样可以加快运算速度:
>>> new_model.eval()
pytorch 不仅支持保存和读取模型状态,还支持保存和读取整个模型包括代码和参数,但我不推荐这种做法,因为使用的时候会看不到模型定义,并且模型依赖的类库或者函数不会一并保存起来所以你还是得预先加载它们否则会出错:
>>> torch.save(model, gzip.GzipFile("model.pt.gz", "wb"))
>>> new_model = torch.load(gzip.GzipFile("model.pt.gz", "rb"))
记录训练集和验证集的正确率变化
我们可以在训练过程中记录训练集和验证集的正确率变化,以观察是否可以收敛,训练速度如何,以及是否发生过拟合问题,以下是代码例子:
# 引用 pytorch 和 pandas 和显示图表使用的 matplotlib
import pandas
import torch
from torch import nn
from matplotlib import pyplot# 定义模型
class MyModel(nn.Module):def __init__(self):super().__init__()self.layer1 = nn.Linear(in_features=8, out_features=100)self.layer2 = nn.Linear(in_features=100, out_features=50)self.layer3 = nn.Linear(in_features=50, out_features=1)def forward(self, x):hidden1 = nn.functional.relu(self.layer1(x))hidden2 = nn.functional.relu(self.layer2(hidden1))y = self.layer3(hidden2)return y# 给随机数生成器分配一个初始值,使得每次运行都可以生成相同的随机数
# 这是为了让训练过程可重现,你也可以选择不这样做
torch.random.manual_seed(0)# 创建模型实例
model = MyModel()# 创建损失计算器
loss_function = torch.nn.MSELoss()# 创建参数调整器
optimizer = torch.optim.SGD(model.parameters(), lr=0.0000001)# 从 csv 读取原始数据集
df = pandas.read_csv('salary.csv')
dataset_tensor = torch.tensor(df.values, dtype=torch.float)# 切分训练集 (60%),验证集 (20%) 和测试集 (20%)
random_indices = torch.randperm(dataset_tensor.shape[0])
traning_indices = random_indices[:int(len(random_indices)*0.6)]
validating_indices = random_indices[int(len(random_indices)*0.6):int(len(random_indices)*0.8):]
testing_indices = random_indices[int(len(random_indices)*0.8):]
traning_set_x = dataset_tensor[traning_indices][:,:-1]
traning_set_y = dataset_tensor[traning_indices][:,-1:]
validating_set_x = dataset_tensor[validating_indices][:,:-1]
validating_set_y = dataset_tensor[validating_indices][:,-1:]
testing_set_x = dataset_tensor[testing_indices][:,:-1]
testing_set_y = dataset_tensor[testing_indices][:,-1:]# 记录训练集和验证集的正确率变化
traning_accuracy_history = []
validating_accuracy_history = []# 开始训练过程
for epoch in range(1, 500):print(f"epoch: {epoch}")# 根据训练集训练并修改参数# 切换模型到训练模式,将会启用自动微分,批次正规化 (BatchNorm) 与 Dropoutmodel.train()traning_accuracy_list = []for batch in range(0, traning_set_x.shape[0], 100):# 切分批次,一次只计算 100 组数据batch_x = traning_set_x[batch:batch+100]batch_y = traning_set_y[batch:batch+100]# 计算预测值predicted = model(batch_x)# 计算损失loss = loss_function(predicted, batch_y)# 从损失自动微分求导函数值loss.backward()# 使用参数调整器调整参数optimizer.step()# 清空导函数值optimizer.zero_grad()# 记录这一个批次的正确率,torch.no_grad 代表临时禁用自动微分功能with torch.no_grad():traning_accuracy_list.append(1 - ((batch_y - predicted).abs() / batch_y).mean().item())traning_accuracy = sum(traning_accuracy_list) / len(traning_accuracy_list)traning_accuracy_history.append(traning_accuracy)print(f"training accuracy: {traning_accuracy}")# 检查验证集# 切换模型到验证模式,将会禁用自动微分,批次正规化 (BatchNorm) 与 Dropoutmodel.eval()predicted = model(validating_set_x)validating_accuracy = 1 - ((validating_set_y - predicted).abs() / validating_set_y).mean()validating_accuracy_history.append(validating_accuracy.item())print(f"validating x: {validating_set_x}, y: {validating_set_y}, predicted: {predicted}")print(f"validating accuracy: {validating_accuracy}")# 检查测试集
predicted = model(testing_set_x)
testing_accuracy = 1 - ((testing_set_y - predicted).abs() / testing_set_y).mean()
print(f"testing x: {testing_set_x}, y: {testing_set_y}, predicted: {predicted}")
print(f"testing accuracy: {testing_accuracy}")# 显示训练集和验证集的正确率变化
pyplot.plot(traning_accuracy_history, label="traning")
pyplot.plot(validating_accuracy_history, label="validing")
pyplot.ylim(0, 1)
pyplot.legend()
pyplot.show()# 手动输入数据预测输出
while True:try:print("enter input:")r = list(map(float, input().split(",")))x = torch.tensor(r).view(1, len(r))print(model(x)[0,0].item())except Exception as e:print("error:", e)
经过 500 轮训练后会生成以下的图表:
我们可以从图表看到训练集和验证集的正确率都随着训练逐渐上升,并且两个正确率非常接近,这代表训练很成功,模型针对训练集掌握了规律并且可以成功预测没有经过训练的验证集,但实际上我们很难会看到这样的图表,这是因为例子中的数据集是精心构建的并且生成了足够大量的数据。
我们还可能会看到以下类型的图表,分别代表不同的状况:
如果有足够的数据,数据遵从某种规律并且杂质较少,划分训练集和验证集的时候分布均匀,并且使用适当的模型,即可达到理想的状况,但实际很难做到😩。通过分析训练集和验证集的正确率变化我们可以定位问题发生在哪里,其中过拟合问题可以用提早停止 (Early Stopping) 的方式解决 (在第一篇文章已经提到过),接下来我们看看如何决定什么时候停止训练。
决定什么时候停止训练
还记得第一篇提到的训练流程吗?我们将会了解如何在代码中实现这个训练流程:
实现判断是否发生过拟合,可以简单的记录历史最高的验证集正确率,如果经过很多次训练都没有刷新最高正确率则结束训练。记录最高正确率的同时我们还需要保存模型的状态,这时模型摸索到了足够多的规律,但是还没有修改参数适应训练集中的杂质,用来预测未知数据可以达到最好的效果。这种手法又称提早停止 (Early Stopping),是机器学习中很常见的手法。
代码实现如下:
# 引用 pytorch 和 pandas 和显示图表使用的 matplotlib
import pandas
import torch
from torch import nn
from matplotlib import pyplot# 定义模型
class MyModel(nn.Module):def __init__(self):super().__init__()self.layer1 = nn.Linear(in_features=8, out_features=100)self.layer2 = nn.Linear(in_features=100, out_features=50)self.layer3 = nn.Linear(in_features=50, out_features=1)def forward(self, x):hidden1 = nn.functional.relu(self.layer1(x))hidden2 = nn.functional.relu(self.layer2(hidden1))y = self.layer3(hidden2)return y# 给随机数生成器分配一个初始值,使得每次运行都可以生成相同的随机数
# 这是为了让训练过程可重现,你也可以选择不这样做
torch.random.manual_seed(0)# 创建模型实例
model = MyModel()# 创建损失计算器
loss_function = torch.nn.MSELoss()# 创建参数调整器
optimizer = torch.optim.SGD(model.parameters(), lr=0.0000001)# 从 csv 读取原始数据集
df = pandas.read_csv('salary.csv')
dataset_tensor = torch.tensor(df.values, dtype=torch.float)# 切分训练集 (60%),验证集 (20%) 和测试集 (20%)
random_indices = torch.randperm(dataset_tensor.shape[0])
traning_indices = random_indices[:int(len(random_indices)*0.6)]
validating_indices = random_indices[int(len(random_indices)*0.6):int(len(random_indices)*0.8):]
testing_indices = random_indices[int(len(random_indices)*0.8):]
traning_set_x = dataset_tensor[traning_indices][:,:-1]
traning_set_y = dataset_tensor[traning_indices][:,-1:]
validating_set_x = dataset_tensor[validating_indices][:,:-1]
validating_set_y = dataset_tensor[validating_indices][:,-1:]
testing_set_x = dataset_tensor[testing_indices][:,:-1]
testing_set_y = dataset_tensor[testing_indices][:,-1:]# 记录训练集和验证集的正确率变化
traning_accuracy_history = []
validating_accuracy_history = []# 记录最高的验证集正确率
validating_accuracy_highest = 0
validating_accuracy_highest_epoch = 0# 开始训练过程
for epoch in range(1, 10000):print(f"epoch: {epoch}")# 根据训练集训练并修改参数# 切换模型到训练模式,将会启用自动微分,批次正规化 (BatchNorm) 与 Dropoutmodel.train()traning_accuracy_list = []for batch in range(0, traning_set_x.shape[0], 100):# 切分批次,一次只计算 100 组数据batch_x = traning_set_x[batch:batch+100]batch_y = traning_set_y[batch:batch+100]# 计算预测值predicted = model(batch_x)# 计算损失loss = loss_function(predicted, batch_y)# 从损失自动微分求导函数值loss.backward()# 使用参数调整器调整参数optimizer.step()# 清空导函数值optimizer.zero_grad()# 记录这一个批次的正确率,torch.no_grad 代表临时禁用自动微分功能with torch.no_grad():traning_accuracy_list.append(1 - ((batch_y - predicted).abs() / batch_y).mean().item())traning_accuracy = sum(traning_accuracy_list) / len(traning_accuracy_list)traning_accuracy_history.append(traning_accuracy)print(f"training accuracy: {traning_accuracy}")# 检查验证集# 切换模型到验证模式,将会禁用自动微分,批次正规化 (BatchNorm) 与 Dropoutmodel.eval()predicted = model(validating_set_x)validating_accuracy = 1 - ((validating_set_y - predicted).abs() / validating_set_y).mean()validating_accuracy_history.append(validating_accuracy.item())print(f"validating x: {validating_set_x}, y: {validating_set_y}, predicted: {predicted}")print(f"validating accuracy: {validating_accuracy}")# 记录最高的验证集正确率与当时的模型状态,判断是否在 100 次训练后仍然没有刷新记录if validating_accuracy > validating_accuracy_highest:validating_accuracy_highest = validating_accuracyvalidating_accuracy_highest_epoch = epochtorch.save(model.state_dict(), "model.pt")print("highest validating accuracy updated")elif epoch - validating_accuracy_highest_epoch > 100:# 在 100 次训练后仍然没有刷新记录,结束训练print("stop training because highest validating accuracy not updated in 100 epoches")break# 使用达到最高正确率时的模型状态
print(f"highest validating accuracy: {validating_accuracy_highest}",f"from epoch {validating_accuracy_highest_epoch}")
model.load_state_dict(torch.load("model.pt"))# 检查测试集
predicted = model(testing_set_x)
testing_accuracy = 1 - ((testing_set_y - predicted).abs() / testing_set_y).mean()
print(f"testing x: {testing_set_x}, y: {testing_set_y}, predicted: {predicted}")
print(f"testing accuracy: {testing_accuracy}")# 显示训练集和验证集的正确率变化
pyplot.plot(traning_accuracy_history, label="traning")
pyplot.plot(validating_accuracy_history, label="validing")
pyplot.ylim(0, 1)
pyplot.legend()
pyplot.show()# 手动输入数据预测输出
while True:try:print("enter input:")r = list(map(float, input().split(",")))x = torch.tensor(r).view(1, len(r))print(model(x)[0,0].item())except Exception as e:print("error:", e)
最终输出如下:
省略开始的输出stop training because highest validating accuracy not updated in 100 epoches
highest validating accuracy: 0.93173748254776 from epoch 645
testing x: tensor([[48., 1., 18., ..., 5., 0., 5.],[22., 1., 2., ..., 2., 1., 2.],[24., 0., 1., ..., 3., 2., 0.],...,[24., 0., 4., ..., 0., 1., 1.],[39., 0., 0., ..., 0., 5., 5.],[36., 0., 5., ..., 3., 0., 3.]]), y: tensor([[14000.],[10500.],[13000.],...,[15500.],[12000.],[19000.]]), predicted: tensor([[15612.1895],[10705.9873],[12577.7988],...,[16281.9277],[10780.5996],[19780.3281]], grad_fn=<AddmmBackward>)
testing accuracy: 0.9330222606658936
训练集与验证集的正确率变化如下,可以看到我们停在了一个很好的地方😸,继续训练下去也不会有什么改进:
改进程序结构
我们还可以对程序结构进行以下的改进:
- 分离准备数据集和训练的过程
- 训练过程中分批读取数据
- 提供接口使用训练好的模型
至此为止我们看到的训练代码都是把准备数据集,训练,训练后评价和使用写在一个程序里面的,这样做容易理解但在实际业务中会比较浪费时间,如果你发现一个模型不适合,需要修改模型那么你得从头开始。我们可以分离准备数据集和训练的过程,首先读取原始数据并且转换到 tensor 对象再保存到硬盘,然后再从硬盘读取 tensor 对象进行训练,这样如果需要修改模型但不需要修改输入输出转换到 tensor 的编码时,可以节省掉第一步。
在实际业务上数据可能会非常庞大,做不到全部读取到内存中再分批次,这时我们可以在读取原始数据并且转换到 tensor 对象的时候进行分批,然后训练的过程中逐批从硬盘读取,这样就可以防止内存不足的问题。
最后我们可以提供一个对外的接口来使用训练好的模型,如果你的程序是 python 写的那么直接调用即可,但如果你的程序是其他语言写的,可能需要先建立一个 python 服务器提供 REST 服务,或者使用 TorchScript 进行跨语言交互,详细可以参考官方的教程。
总结起来我们会拆分以下过程:
- 读取原始数据集并转换到 tensor 对象
- 分批次保存 tensor 对象到硬盘
- 分批次从硬盘读取 tensor 对象并进行训练
- 训练时保存模型状态到硬盘 (一般选择保存验证集正确率最高时的模型状态)
- 提供接口使用训练好的模型
以下是改进后的示例代码:
import os
import sys
import pandas
import torch
import gzip
import itertools
from torch import nn
from matplotlib import pyplotclass MyModel(nn.Module):"""根据码农条件预测工资的模型"""def __init__(self):super().__init__()self.layer1 = nn.Linear(in_features=8, out_features=100)self.layer2 = nn.Linear(in_features=100, out_features=50)self.layer3 = nn.Linear(in_features=50, out_features=1)def forward(self, x):hidden1 = nn.functional.relu(self.layer1(x))hidden2 = nn.functional.relu(self.layer2(hidden1))y = self.layer3(hidden2)return ydef save_tensor(tensor, path):"""保存 tensor 对象到文件"""torch.save(tensor, gzip.GzipFile(path, "wb"))def load_tensor(path):"""从文件读取 tensor 对象"""return torch.load(gzip.GzipFile(path, "rb"))def prepare():"""准备训练"""# 数据集转换到 tensor 以后会保存在 data 文件夹下if not os.path.isdir("data"):os.makedirs("data")# 从 csv 读取原始数据集,分批每次读取 2000 行for batch, df in enumerate(pandas.read_csv('salary.csv', chunksize=2000)):dataset_tensor = torch.tensor(df.values, dtype=torch.float)# 切分训练集 (60%),验证集 (20%) 和测试集 (20%)random_indices = torch.randperm(dataset_tensor.shape[0])traning_indices = random_indices[:int(len(random_indices)*0.6)]validating_indices = random_indices[int(len(random_indices)*0.6):int(len(random_indices)*0.8):]testing_indices = random_indices[int(len(random_indices)*0.8):]training_set = dataset_tensor[traning_indices]validating_set = dataset_tensor[validating_indices]testing_set = dataset_tensor[testing_indices]# 保存到硬盘save_tensor(training_set, f"data/training_set.{batch}.pt")save_tensor(validating_set, f"data/validating_set.{batch}.pt")save_tensor(testing_set, f"data/testing_set.{batch}.pt")print(f"batch {batch} saved")def train():"""开始训练"""# 创建模型实例model = MyModel()# 创建损失计算器loss_function = torch.nn.MSELoss()# 创建参数调整器optimizer = torch.optim.SGD(model.parameters(), lr=0.0000001)# 记录训练集和验证集的正确率变化traning_accuracy_history = []validating_accuracy_history = []# 记录最高的验证集正确率validating_accuracy_highest = 0validating_accuracy_highest_epoch = 0# 读取批次的工具函数def read_batches(base_path):for batch in itertools.count():path = f"{base_path}.{batch}.pt"if not os.path.isfile(path):breakyield load_tensor(path)# 计算正确率的工具函数def calc_accuracy(actual, predicted):return max(0, 1 - ((actual - predicted).abs() / actual.abs()).mean().item())# 开始训练过程for epoch in range(1, 10000):print(f"epoch: {epoch}")# 根据训练集训练并修改参数# 切换模型到训练模式,将会启用自动微分,批次正规化 (BatchNorm) 与 Dropoutmodel.train()traning_accuracy_list = []for batch in read_batches("data/training_set"):# 切分小批次,有助于泛化模型for index in range(0, batch.shape[0], 100):# 划分输入和输出batch_x = batch[index:index+100,:-1]batch_y = batch[index:index+100,-1:]# 计算预测值predicted = model(batch_x)# 计算损失loss = loss_function(predicted, batch_y)# 从损失自动微分求导函数值loss.backward()# 使用参数调整器调整参数optimizer.step()# 清空导函数值optimizer.zero_grad()# 记录这一个批次的正确率,torch.no_grad 代表临时禁用自动微分功能with torch.no_grad():traning_accuracy_list.append(calc_accuracy(batch_y, predicted))traning_accuracy = sum(traning_accuracy_list) / len(traning_accuracy_list)traning_accuracy_history.append(traning_accuracy)print(f"training accuracy: {traning_accuracy}")# 检查验证集# 切换模型到验证模式,将会禁用自动微分,批次正规化 (BatchNorm) 与 Dropoutmodel.eval()validating_accuracy_list = []for batch in read_batches("data/validating_set"):validating_accuracy_list.append(calc_accuracy(batch[:,-1:], model(batch[:,:-1])))validating_accuracy = sum(validating_accuracy_list) / len(validating_accuracy_list)validating_accuracy_history.append(validating_accuracy)print(f"validating accuracy: {validating_accuracy}")# 记录最高的验证集正确率与当时的模型状态,判断是否在 100 次训练后仍然没有刷新记录if validating_accuracy > validating_accuracy_highest:validating_accuracy_highest = validating_accuracyvalidating_accuracy_highest_epoch = epochsave_tensor(model.state_dict(), "model.pt")print("highest validating accuracy updated")elif epoch - validating_accuracy_highest_epoch > 100:# 在 100 次训练后仍然没有刷新记录,结束训练print("stop training because highest validating accuracy not updated in 100 epoches")break# 使用达到最高正确率时的模型状态print(f"highest validating accuracy: {validating_accuracy_highest}",f"from epoch {validating_accuracy_highest_epoch}")model.load_state_dict(load_tensor("model.pt"))# 检查测试集testing_accuracy_list = []for batch in read_batches("data/testing_set"):testing_accuracy_list.append(calc_accuracy(batch[:,-1:], model(batch[:,:-1])))testing_accuracy = sum(testing_accuracy_list) / len(testing_accuracy_list)print(f"testing accuracy: {testing_accuracy}")# 显示训练集和验证集的正确率变化pyplot.plot(traning_accuracy_history, label="traning")pyplot.plot(validating_accuracy_history, label="validing")pyplot.ylim(0, 1)pyplot.legend()pyplot.show()def eval_model():"""使用训练好的模型"""parameters = ["Age","Gender (0: Male, 1: Female)","Years of work experience","Java Skill (0 ~ 5)","NET Skill (0 ~ 5)","JS Skill (0 ~ 5)","CSS Skill (0 ~ 5)","HTML Skill (0 ~ 5)"]# 创建模型实例,加载训练好的状态,然后切换到验证模式model = MyModel()model.load_state_dict(load_tensor("model.pt"))model.eval()# 询问输入并预测输出while True:try:x = torch.tensor([int(input(f"Your {p}: ")) for p in parameters], dtype=torch.float)# 转换到 1 行 1 列的矩阵,这里其实可以不转换但推荐这么做,因为不是所有模型都支持非批次输入x = x.view(1, len(x))y = model(x)print("Your estimated salary:", y[0,0].item(), "\n")except Exception as e:print("error:", e)def main():"""主函数"""if len(sys.argv) < 2:print(f"Please run: {sys.argv[0]} prepare|train|eval")exit()# 给随机数生成器分配一个初始值,使得每次运行都可以生成相同的随机数# 这是为了让过程可重现,你也可以选择不这样做torch.random.manual_seed(0)# 根据命令行参数选择操作operation = sys.argv[1]if operation == "prepare":prepare()elif operation == "train":train()elif operation == "eval":eval_model()else:raise ValueError(f"Unsupported operation: {operation}")if __name__ == "__main__":main()
执行以下命令即可走一遍完整的流程,如果你需要调整模型,可以直接重新运行 train 避免 prepare 的时间消耗:
python3 example.py prepare
python3 example.py train
python3 example.py eval
注意以上代码在打乱数据集和分批的处理上与以往的代码不一样,以上的代码会分段读取 csv 文件,然后对每一段打乱再切分训练集,验证集和测试集,这样做同样可以保证数据在各个集合中分布均匀。最终训练集和验证集的正确率变化如下:
正规化输入和输出值
目前为止我们在训练的时候都是直接给模型原始的输入值,然后用原始的输出值去调整参数,这样做的问题是,如果输入值非常大导函数值也会非常大,如果输出值非常大需要调整参数的次数会非常多,过去我们用一个非常非常小的学习比率 (0.0000001) 来避开这个问题,但其实有更好的办法,那就是正规化输入和输出值。这里的正规化指的是让输入值和输出值按一定比例缩放,让大部分的值都落在 -1 ~ 1 的区间中。在根据码农条件预测工资的例子中,我们可以把年龄和工作经验年数乘以 0.01 (范围 0 ~ 100 年),各项技能乘以 0.02 (范围 0 ~ 5),工资乘以 0.0001 (以万为单位),对 dataset_tensor
进行以下操作即可实现:
# 对每一行乘以指定的系数
dataset_tensor *= torch.tensor([0.01, 1, 0.01, 0.2, 0.2, 0.2, 0.2, 0.2, 0.0001])
然后再修改学习比率为 0.01:
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
比较训练 300 次的正确率变化如下:
正规化输入和输出值前
正规化输入和输出值后
可以看到效果相当惊人😈,正规化输入和输出值后训练速度变快了并且正确率的变化曲线平滑了很多。实际上这是必须做的,部分数据集如果没有经过正规化根本无法学习,让模型接收和输出更小的值 (-1 ~ 1 的区间) 可以防止导函数值爆炸和使用更高的学习比率加快训练速度。
此外,别忘了在使用模型的时候缩放输入和输出值:
x = torch.tensor([int(input(f"Your {p}: ")) for p in parameters], dtype=torch.float)
x *= torch.tensor([0.01, 1, 0.01, 0.2, 0.2, 0.2, 0.2, 0.2])
# 转换到 1 行 1 列的矩阵,这里其实可以不转换但推荐这么做,因为不是所有模型都支持非批次输入
x = x.view(1, len(x))
y = model(x) * 10000
print("Your estimated salary:", y[0,0].item(), "\n")
使用 Dropout 帮助泛化模型
在之前的内容中已经提到过,如果模型能力过于强大或者数据杂质较多,则模型有可能会适应数据中的杂质以达到更高的正确率 (过拟合现象),这时候虽然训练集的正确率会上升,但验证集的正确率会维持甚至下降,模型应对未知数据的能力会降低。防止过拟合现象,增强模型应对未知数据的能力又称泛化模型 (Generalize Model),泛化模型的手段之一是使用 Dropout,Dropout 会在训练过程中随机屏蔽一部分的神经元,让这些神经元的输出为 0,同时增幅没有被屏蔽的神经元输出让输出值合计接近原有的水平,这样做的好处是模型会尝试摸索怎样在一部分神经元被屏蔽后仍然可以正确预测结果 (减弱跨层神经元之间的关联),最终导致模型更充分的掌握数据的规律。
下图是使用 Dropout 以后的神经元网络例子 (3 输入 2 输出,3 层每层各 5 隐藏值):
接下来我们看看在 Pytorch 中怎么使用 Dropout:
# 引用 pytorch 类库
>>> import torch# 创建屏蔽 20% 的 Dropout 函数
>>> dropout = torch.nn.Dropout(0.2)# 定义一个 tensor (假设这个 tensor 是某个神经元网络层的输出结果)
>>> a = torch.tensor(range(1, 11), dtype=torch.float)
>>> a
tensor([ 1., 2., 3., 4., 5., 6., 7., 8., 9., 10.])# 应用 Dropout 函数
# 我们可以看到没有屏蔽的值都会相应的增加 (除以 0.8) 以让合计值维持原有的水平
# 此外屏蔽的数量会根据概率浮动,不一定 100% 等于我们设置的比例 (这里有屏蔽 1 个值的也有屏蔽 3 个值的)
>>> dropout(a)
tensor([ 0.0000, 2.5000, 3.7500, 5.0000, 6.2500, 7.5000, 8.7500, 10.0000,11.2500, 12.5000])
>>> dropout(a)
tensor([ 1.2500, 2.5000, 3.7500, 5.0000, 6.2500, 7.5000, 8.7500, 0.0000,11.2500, 0.0000])
>>> dropout(a)
tensor([ 1.2500, 2.5000, 3.7500, 5.0000, 6.2500, 7.5000, 8.7500, 0.0000,11.2500, 12.5000])
>>> dropout(a)
tensor([ 1.2500, 2.5000, 3.7500, 5.0000, 6.2500, 7.5000, 0.0000, 10.0000,11.2500, 0.0000])
>>> dropout(a)
tensor([ 1.2500, 2.5000, 3.7500, 5.0000, 0.0000, 7.5000, 8.7500, 10.0000,11.2500, 0.0000])
>>> dropout(a)
tensor([ 1.2500, 2.5000, 0.0000, 5.0000, 0.0000, 7.5000, 8.7500, 10.0000,11.2500, 12.5000])
>>> dropout(a)
tensor([ 0.0000, 2.5000, 3.7500, 5.0000, 6.2500, 7.5000, 0.0000, 10.0000,0.0000, 0.0000])
接下来我们看看怎样应用 Dropout 到模型中,首先我们重现一下过拟合现象,增加模型的神经元数量并且减少训练集的数据量即可:
模型部分的代码:
class MyModel(nn.Module):"""根据码农条件预测工资的模型"""def __init__(self):super().__init__()self.layer1 = nn.Linear(in_features=8, out_features=200)self.layer2 = nn.Linear(in_features=200, out_features=100)self.layer3 = nn.Linear(in_features=100, out_features=1)def forward(self, x):hidden1 = nn.functional.relu(self.layer1(x))hidden2 = nn.functional.relu(self.layer2(hidden1))y = self.layer3(hidden2)return y
训练部分的代码 (每个批次只训练前 16 个数据):
for batch in read_batches("data/training_set"):# 切分小批次,有助于泛化模型for index in range(0, batch.shape[0], 16):# 划分输入和输出batch_x = batch[index:index+16,:-1]batch_y = batch[index:index+16,-1:]# 计算预测值predicted = model(batch_x)# 计算损失loss = loss_function(predicted, batch_y)# 从损失自动微分求导函数值loss.backward()# 使用参数调整器调整参数optimizer.step()# 清空导函数值optimizer.zero_grad()# 记录这一个批次的正确率,torch.no_grad 代表临时禁用自动微分功能with torch.no_grad():traning_accuracy_list.append(calc_accuracy(batch_y, predicted))# 只训练前 16 个数据break
固定训练 1000 次以后的正确率:
training accuracy: 0.9706422178819776
validating accuracy: 0.8514168351888657
highest validating accuracy: 0.8607834208011628 from epoch 223
testing accuracy: 0.8603586450219154
以及正确率变化的趋势:
试着在模型中加入两个 Dropout,分别对应第一层与第二层的输出 (隐藏值):
class MyModel(nn.Module):"""根据码农条件预测工资的模型"""def __init__(self):super().__init__()self.layer1 = nn.Linear(in_features=8, out_features=200)self.layer2 = nn.Linear(in_features=200, out_features=100)self.layer3 = nn.Linear(in_features=100, out_features=1)self.dropout1 = nn.Dropout(0.2)self.dropout2 = nn.Dropout(0.2)def forward(self, x):hidden1 = self.dropout1(nn.functional.relu(self.layer1(x)))hidden2 = self.dropout2(nn.functional.relu(self.layer2(hidden1)))y = self.layer3(hidden2)return y
这时候再来训练会得出以下的正确率:
training accuracy: 0.9326518730819225
validating accuracy: 0.8692235469818115
highest validating accuracy: 0.8728838726878166 from epoch 867
testing accuracy: 0.8733032837510109
以及正确率变化的趋势:
我们可以看到训练集的正确率没有盲目的上升,并且验证集与测试集的正确率都各上升了 1% 以上,说明 Dropout 是有一定效果的。
使用 Dropout 时应该注意以下的几点:
- Dropout 应该针对隐藏值使用,不能放在第一层的前面 (针对输入) 或者最后一层的后面 (针对输出)
- Dropout 应该放在激活函数后面 (因为激活函数是神经元的一部分)
- Dropout 只应该在训练过程中使用,评价或实际使用模型时应该调用
model.eval()
切换模型到评价模式,以禁止 Dropout - Dropout 函数应该定义为模型的成员,这样调用
model.eval()
可以索引到模型对应的所有 Dropout 函数 - Dropout 的屏蔽比例没有最佳值,你可以针对当前的数据和模型多试几次找出最好的结果
提出 Dropout 手法的原始论文在这里,如果你有兴趣可以查看。
使用 BatchNorm 正规化批次
BatchNorm 是另外一种提升训练效果的手法,在一些场景下可以提升训练效率和抑制过拟合,BatchNorm 和 Dropout 一样针对隐藏值使用,会对每个批次的各项值 (每一列) 进行正规化,计算公式如下:
总结来说就是让每一列中的各个值减去这一列的平均值,然后除以这一列的标准差,再按一定比例调整。
在 python 中使用 BatchNorm 的例子如下:
# 创建 batchnorm 函数,3 代表列数
>>> batchnorm = torch.nn.BatchNorm1d(3)# 查看 batchnorm 函数内部的权重与偏移
>>> list(batchnorm.parameters())
[Parameter containing:
tensor([1., 1., 1.], requires_grad=True), Parameter containing:
tensor([0., 0., 0.], requires_grad=True)]# 随机创建一个 10 行 3 列的 tensor
>>> a = torch.rand((10, 3))
>>> a
tensor([[0.9643, 0.6933, 0.0039],[0.3967, 0.8239, 0.3490],[0.4011, 0.8903, 0.3053],[0.0666, 0.5766, 0.4976],[0.4928, 0.1403, 0.8900],[0.7317, 0.9461, 0.1816],[0.4461, 0.9987, 0.8324],[0.3714, 0.6550, 0.9961],[0.4852, 0.7415, 0.1779],[0.6876, 0.1538, 0.3429]])# 应用 batchnorm 函数
>>> batchnorm(a)
tensor([[ 1.9935, 0.1096, -1.4156],[-0.4665, 0.5665, -0.3391],[-0.4477, 0.7985, -0.4754],[-1.8972, -0.2986, 0.1246],[-0.0501, -1.8245, 1.3486],[ 0.9855, 0.9939, -0.8611],[-0.2523, 1.1776, 1.1691],[-0.5761, -0.0243, 1.6798],[-0.0831, 0.2783, -0.8727],[ 0.7941, -1.7770, -0.3581]], grad_fn=<NativeBatchNormBackward>)# 手动重现 batchnorm 对第一列的计算
>>> aa = a[:,:1]
>>> aa
tensor([[0.9643],[0.3967],[0.4011],[0.0666],[0.4928],[0.7317],[0.4461],[0.3714],[0.4852],[0.6876]])
>>> (aa - aa.mean()) / (((aa - aa.mean()) ** 2).mean() + 0.00001).sqrt()
tensor([[ 1.9935],[-0.4665],[-0.4477],[-1.8972],[-0.0501],[ 0.9855],[-0.2523],[-0.5761],[-0.0831],[ 0.7941]])
修改模型使用 BatchNorm 的代码如下:
class MyModel(nn.Module):"""根据码农条件预测工资的模型"""def __init__(self):super().__init__()self.layer1 = nn.Linear(in_features=8, out_features=200)self.layer2 = nn.Linear(in_features=200, out_features=100)self.layer3 = nn.Linear(in_features=100, out_features=1)self.batchnorm1 = nn.BatchNorm1d(200)self.batchnorm2 = nn.BatchNorm1d(100)self.dropout1 = nn.Dropout(0.1)self.dropout2 = nn.Dropout(0.1)def forward(self, x):hidden1 = self.dropout1(self.batchnorm1(nn.functional.relu(self.layer1(x))))hidden2 = self.dropout2(self.batchnorm2(nn.functional.relu(self.layer2(hidden1))))y = self.layer3(hidden2)return y
需要同时调整学习比率:
# 创建参数调整器
optimizer = torch.optim.SGD(model.parameters(), lr=0.05)
固定训练 1000 次的结果如下,可以看到在这个场景下 BatchNorm 没有发挥作用🤕,反而减慢了学习速度和影响可达到的最高正确率 (你可以试试增加训练次数):
training accuracy: 0.9048486271500588
validating accuracy: 0.8341873311996459
highest validating accuracy: 0.8443503141403198 from epoch 946
testing accuracy: 0.8452585405111313
使用 BatchNorm 时应该注意以下的几点:
- BatchNorm 应该针对隐藏值使用,和 Dropout 一样
- BatchNorm 需要指定隐藏值数量,应该与对应层的输出数量匹配
- BatchNorm 应该放在 Dropout 前面,有部分人会选择把 BatchNorm 放在激活函数前,也有部分人选择放在激活函数后
- Linear => ReLU => BatchNorm => Dropout
- Linear => BatchNorm => ReLU => Dropout
- BatchNorm 只应该在训练过程中使用,和 Dropout 一样
- BatchNorm 函数应该定义为模型的成员,和 Dropout 一样
- 使用 BatchNorm 的时候应该相应的减少 Dropout 的屏蔽比例
- 部分场景可能不适用 BatchNorm (据说更适用于对象识别和图片分类),需要实践才能出真知 ☭
提出 BatchNorm 手法的原始论文在这里,如果你有兴趣可以查看。
理解模型的 eval 和 train 模式
在前面的例子中我们使用了 eval
和 train
函数切换模型到评价模式和训练模式,评价模式会禁用自动微分,Dropout 和 BatchNorm,那么这两个模式是如何实现的呢?
pytorch 的模型都基于 torch.nn.Module
这个类,不仅是我们自己定义的模型,nn.Sequential
, nn.Linear
, nn.ReLU
, nn.Dropout
, nn.BatchNorm1d
等等的类型都会基于 torch.nn.Module
,torch.nn.Module
有一个 training
成员代表模型是否处于训练模式,而 eval
函数用于递归设置所有 Module
的 training
为 False
,train
函数用于递归设置所有 Module
的 training
为 True。我们可以手动设置这个成员看看是否能起到相同效果:
>>> a = torch.tensor(range(1, 11), dtype=torch.float)
>>> dropout = torch.nn.Dropout(0.2)>>> dropout.training = False
>>> dropout(a)
tensor([ 1., 2., 3., 4., 5., 6., 7., 8., 9., 10.])>>> dropout.training = True
>>> dropout(a)
tensor([ 1.2500, 2.5000, 3.7500, 0.0000, 0.0000, 7.5000, 8.7500, 10.0000,0.0000, 12.5000])
理解这一点后,你可以在模型中添加只在训练或者评价的时候执行的代码,根据 self.training
判断即可。
最终代码
根据码农条件预测工资的最终代码如下:
import os
import sys
import pandas
import torch
import gzip
import itertools
from torch import nn
from matplotlib import pyplotclass MyModel(nn.Module):"""根据码农条件预测工资的模型"""def __init__(self):super().__init__()self.layer1 = nn.Linear(in_features=8, out_features=200)self.layer2 = nn.Linear(in_features=200, out_features=100)self.layer3 = nn.Linear(in_features=100, out_features=1)self.batchnorm1 = nn.BatchNorm1d(200)self.batchnorm2 = nn.BatchNorm1d(100)self.dropout1 = nn.Dropout(0.1)self.dropout2 = nn.Dropout(0.1)def forward(self, x):hidden1 = self.dropout1(self.batchnorm1(nn.functional.relu(self.layer1(x))))hidden2 = self.dropout2(self.batchnorm2(nn.functional.relu(self.layer2(hidden1))))y = self.layer3(hidden2)return ydef save_tensor(tensor, path):"""保存 tensor 对象到文件"""torch.save(tensor, gzip.GzipFile(path, "wb"))def load_tensor(path):"""从文件读取 tensor 对象"""return torch.load(gzip.GzipFile(path, "rb"))def prepare():"""准备训练"""# 数据集转换到 tensor 以后会保存在 data 文件夹下if not os.path.isdir("data"):os.makedirs("data")# 从 csv 读取原始数据集,分批每次读取 2000 行for batch, df in enumerate(pandas.read_csv('salary.csv', chunksize=2000)):dataset_tensor = torch.tensor(df.values, dtype=torch.float)# 正规化输入和输出dataset_tensor *= torch.tensor([0.01, 1, 0.01, 0.2, 0.2, 0.2, 0.2, 0.2, 0.0001])# 切分训练集 (60%),验证集 (20%) 和测试集 (20%)random_indices = torch.randperm(dataset_tensor.shape[0])traning_indices = random_indices[:int(len(random_indices)*0.6)]validating_indices = random_indices[int(len(random_indices)*0.6):int(len(random_indices)*0.8):]testing_indices = random_indices[int(len(random_indices)*0.8):]training_set = dataset_tensor[traning_indices]validating_set = dataset_tensor[validating_indices]testing_set = dataset_tensor[testing_indices]# 保存到硬盘save_tensor(training_set, f"data/training_set.{batch}.pt")save_tensor(validating_set, f"data/validating_set.{batch}.pt")save_tensor(testing_set, f"data/testing_set.{batch}.pt")print(f"batch {batch} saved")def train():"""开始训练"""# 创建模型实例model = MyModel()# 创建损失计算器loss_function = torch.nn.MSELoss()# 创建参数调整器optimizer = torch.optim.SGD(model.parameters(), lr=0.05)# 记录训练集和验证集的正确率变化traning_accuracy_history = []validating_accuracy_history = []# 记录最高的验证集正确率validating_accuracy_highest = 0validating_accuracy_highest_epoch = 0# 读取批次的工具函数def read_batches(base_path):for batch in itertools.count():path = f"{base_path}.{batch}.pt"if not os.path.isfile(path):breakyield load_tensor(path)# 计算正确率的工具函数def calc_accuracy(actual, predicted):return max(0, 1 - ((actual - predicted).abs() / actual.abs()).mean().item())# 开始训练过程for epoch in range(1, 10000):print(f"epoch: {epoch}")# 根据训练集训练并修改参数# 切换模型到训练模式,将会启用自动微分,批次正规化 (BatchNorm) 与 Dropoutmodel.train()traning_accuracy_list = []for batch in read_batches("data/training_set"):# 切分小批次,有助于泛化模型for index in range(0, batch.shape[0], 100):# 划分输入和输出batch_x = batch[index:index+100,:-1]batch_y = batch[index:index+100,-1:]# 计算预测值predicted = model(batch_x)# 计算损失loss = loss_function(predicted, batch_y)# 从损失自动微分求导函数值loss.backward()# 使用参数调整器调整参数optimizer.step()# 清空导函数值optimizer.zero_grad()# 记录这一个批次的正确率,torch.no_grad 代表临时禁用自动微分功能with torch.no_grad():traning_accuracy_list.append(calc_accuracy(batch_y, predicted))traning_accuracy = sum(traning_accuracy_list) / len(traning_accuracy_list)traning_accuracy_history.append(traning_accuracy)print(f"training accuracy: {traning_accuracy}")# 检查验证集# 切换模型到验证模式,将会禁用自动微分,批次正规化 (BatchNorm) 与 Dropoutmodel.eval()validating_accuracy_list = []for batch in read_batches("data/validating_set"):validating_accuracy_list.append(calc_accuracy(batch[:,-1:], model(batch[:,:-1])))validating_accuracy = sum(validating_accuracy_list) / len(validating_accuracy_list)validating_accuracy_history.append(validating_accuracy)print(f"validating accuracy: {validating_accuracy}")# 记录最高的验证集正确率与当时的模型状态,判断是否在 100 次训练后仍然没有刷新记录if validating_accuracy > validating_accuracy_highest:validating_accuracy_highest = validating_accuracyvalidating_accuracy_highest_epoch = epochsave_tensor(model.state_dict(), "model.pt")print("highest validating accuracy updated")elif epoch - validating_accuracy_highest_epoch > 100:# 在 100 次训练后仍然没有刷新记录,结束训练print("stop training because highest validating accuracy not updated in 100 epoches")break# 使用达到最高正确率时的模型状态print(f"highest validating accuracy: {validating_accuracy_highest}",f"from epoch {validating_accuracy_highest_epoch}")model.load_state_dict(load_tensor("model.pt"))# 检查测试集testing_accuracy_list = []for batch in read_batches("data/testing_set"):testing_accuracy_list.append(calc_accuracy(batch[:,-1:], model(batch[:,:-1])))testing_accuracy = sum(testing_accuracy_list) / len(testing_accuracy_list)print(f"testing accuracy: {testing_accuracy}")# 显示训练集和验证集的正确率变化pyplot.plot(traning_accuracy_history, label="traning")pyplot.plot(validating_accuracy_history, label="validing")pyplot.ylim(0, 1)pyplot.legend()pyplot.show()def eval_model():"""使用训练好的模型"""parameters = ["Age","Gender (0: Male, 1: Female)","Years of work experience","Java Skill (0 ~ 5)","NET Skill (0 ~ 5)","JS Skill (0 ~ 5)","CSS Skill (0 ~ 5)","HTML Skill (0 ~ 5)"]# 创建模型实例,加载训练好的状态,然后切换到验证模式model = MyModel()model.load_state_dict(load_tensor("model.pt"))model.eval()# 询问输入并预测输出while True:try:x = torch.tensor([int(input(f"Your {p}: ")) for p in parameters], dtype=torch.float)# 正规化输入x *= torch.tensor([0.01, 1, 0.01, 0.2, 0.2, 0.2, 0.2, 0.2])# 转换到 1 行 1 列的矩阵,这里其实可以不转换但推荐这么做,因为不是所有模型都支持非批次输入x = x.view(1, len(x))# 预测输出y = model(x)# 反正规化输出y *= 10000print("Your estimated salary:", y[0,0].item(), "\n")except Exception as e:print("error:", e)def main():"""主函数"""if len(sys.argv) < 2:print(f"Please run: {sys.argv[0]} prepare|train|eval")exit()# 给随机数生成器分配一个初始值,使得每次运行都可以生成相同的随机数# 这是为了让过程可重现,你也可以选择不这样做torch.random.manual_seed(0)# 根据命令行参数选择操作operation = sys.argv[1]if operation == "prepare":prepare()elif operation == "train":train()elif operation == "eval":eval_model()else:raise ValueError(f"Unsupported operation: {operation}")if __name__ == "__main__":main()
最终训练结果如下,验证集和测试集正确率达到了 94.3% (前一篇分别是 93.3% 和 93.1%):
epoch: 848
training accuracy: 0.929181088420252
validating accuracy: 0.9417830203473568
stop training because highest validating accuracy not updated in 100 epoches
highest validating accuracy: 0.9437697219848633 from epoch 747
testing accuracy: 0.9438129015266895
正确率变化如下:
算是圆满成功了叭🥳。
写在最后
在这一篇我们看到了各种改进训练过程和改善训练效果的手法,预测了各种各样码农的工资🙀,接下来我们可以试着做一些不同的事情了。下一篇会介绍递归模型 RNN,LSTM 与 GRU,它们可以用于处理不定长度的数据,实现根据上下文分类,预测趋势,自动补全等功能。
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
相关文章
- WINSrv2016 及win10装dotNETframwork3.5 /4.6
大家应该都有碰到过,在Win10专业版上面运行程序的时候,经常会弹窗提示缺乏某些组件,无法运行或者无法安装的情况。这是因为软件在开发过程中借助一些运行工具,所以要使用这类软件需要对其支持的工具进行安装。最出名的莫过于.net …...
2024/5/8 19:47:19 - 《计算机组网试验-TCP协议分析|CSDN创作打卡》杭州电子科技大学
实验过程及步骤(可另附页,使用网络拓扑图等辅助说明): 实验介绍。介绍本次实验的内容,介绍本次实验要抓的包,TCP协议是在计算机网络中使用最广泛的协议,很多的应用服务如FTP,HTTP,SMTP等在传输…...
2024/5/8 16:29:54 - Linux日志管理
常见的日志文件 系统中的日志信息一般都是先存储在内存当中,然后再有rsyslog服务将对应的日志信息进行采集,然后再根据rsyslog.conf配置文件将对应的日志信息存储到对应文件当中 rsyslog.conf 在这个配置文件当中,我们可以制定这样格式的规则…...
2024/5/8 13:59:13 - 232. 用栈实现队列
地址: 力扣https://leetcode-cn.com/problems/implement-queue-using-stacks/ 题目: 请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty): 实现 MyQueue 类ÿ…...
2024/5/1 4:14:15 - 《Redis操作Python系列、setrange修改字符串|CSDN创作打卡》
导读 其实字符串修改在很多时候还都是很实用的,但是一般存储的字符串也不大,很多人就给直接替换整个字符串了,咱们python提供了一个修改的函数setrange,可以支持本操作。 setrange语法: setrange(name, offset, val…...
2024/5/2 12:56:06 - 双Y轴柱状、曲线图
dataXName["淑阳镇","","","",""]option{tooltip: {trigger: "axis",// "axisPointer": {// "type": "cross",// "crossStyle": {// "color"…...
2024/5/5 3:57:03 - P1309 [NOIP2011 普及组] 瑞士轮
题目背景 在双人对决的竞技性比赛,如乒乓球、羽毛球、国际象棋中,最常见的赛制是淘汰赛和循环赛。前者的特点是比赛场数少,每场都紧张刺激,但偶然性较高。后者的特点是较为公平,偶然性较低,但比赛过程往往…...
2024/5/8 12:17:27 - 【央视网发布】2022年占据C位的“跨圈生产力”:元宇宙报告权威发布
元宇宙 近日,中国传媒大学媒体融合与传播国家重点实验室新媒体研究院与新浪新闻、央视网智媒学院共同发布《2022元宇宙研究报告:多元视角》,从十个视角深入观察社会实践,对前沿趋势变化提供研究思路。 元宇宙概念和产业的发展还…...
2024/4/14 7:02:16 - 2022.1.24
JAR文件的目的是把类和相关的资源封装到压缩的归档文件中,而对于WAR文件来说,一个WAR文件代表了一个Web应用程序,它可以包含 Servlet、HTML页面、Java类、图像文件,以及组成Web应用程序的其他资源,而不仅仅是类的归档文…...
2024/5/8 6:38:11 - 力扣刷题百天计划 Day12 环形链表 力扣题库第141题 C# 哈希表+双指针
学习目标: 我会持续更新我独特的算法思路,希望能给大家带来不一样的思维拓展! 如果大家感觉有帮助的话,欢迎点赞关注支持哦! 你们的鼓励是我坚持下去的动力! !!! 力扣题…...
2024/4/14 7:02:57 - Hadoop_Yarn调度器和调度算法
目录 1.先进先出调度器(FIFO) 2.容量调度器(Capacity Scheduler) 3.公平调度器(Fair Scheduler) (1)特点 (2)缺额含义 (3) 队列资源分…...
2024/5/2 5:46:29 - #python文件操作及案例演示实战
找到这个文件 , 双击打开它 open(文件路径 , mode ’’,encode’’) 文件路径: 1.绝对路径 d:/test/xxxx/txt 2.相对路径 相对于当前你的程序所在的文件夹 ../ 返回上一层文件夹mode: r: read 读取 w: write 写 a: append 追加写入 b: 读写…...
2024/4/14 7:02:52 - (每日一练java)插入区间
插入区间 给你一个 无重叠的 ,按照区间起始端点排序的区间列表。 在列表中插入一个新的区间,你需要确保列表中的区间仍然有序且不重叠(如果有必要的话,可以合并区间)。 示例 1: 输入:interva…...
2024/5/6 23:04:17 - SpringBoot集成RestTemplate请求高德地图API
加入相关依赖与spring-boot集成 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId> </dependency> RestTemplate连接池超时等配置 import org.apache.http.client.HttpClient…...
2024/5/4 15:28:51 - 车载开发之CarLife CarPlay HiCar
百度、华为的过渡方案Carlife、HiCar能否转正?_sinat_32970179的博客-CSDN博客_mmi与设备之间的wlan连接无法建立文 | 魏启扬来源 | 智能相对论(ID:aixdlun)手机与车机,到底是朋友还是冤家?一面是时任阿里巴…...
2024/5/9 2:08:56 - 倍福KL1104、KL2021模块IO输入输出通道灯不亮,PLC没信号输入
在测试过程中,发现KL1104、KL2021模块没有输入输出反应,即有信号灯也没亮,后发现是线没接好,本文说明其具体原因。 操作流程 1.1. 问题分析 在短接模块端子,即用24V电源连接KL1104通道,或者PLC输出到KL2012模块通道中,信号都不亮,如下所示: 1.2. 解决方案 该原因是因为…...
2024/4/14 7:02:52 - 22 1 23 C语言学习总结 (自定义类型)
一 位段 1 位段的声明和结构是类似的,有两个不同: (1) 位段的成员必须是 int、unsigned int 或signed int 。 (2) 位段的成员名后边有一个冒号和一个数字。 例如: struct A {int _a:2;int _b:5;int _c:10;int _d:30; };A就是一个位段类型…...
2024/4/14 7:02:47 - 人脸识别之目标追踪识别
人脸识别之目标追踪识别 1.开发工具 Python版本:Anaconda 3.8环境 开发软件:Pycharm社区版 相关模块:sys模块,pypinyin模块,os模块,opencv-contrib-python 模块,opencv-python模块。 模型&#…...
2024/5/4 5:33:46 - orcal数据库笔记,一些基础笔记,后期会慢慢完善和补充
数据库 目录 数据库 1.数据库简介 2.1表空间 2.2创建表空间 2.3创建用户 2.4用户授权 2.5库的备份、还原 3表 3.1创建表 3.2删除表、数据 3.3表字段语句 3.4表查询 3.5表连接查询 3.6限制返回条数 3.7数据类型 3.8查询 3.9删除 3.10修改 3.11添加 4.视图 …...
2024/5/7 17:22:54 - (D3DFR)Accurate 3D Face Reconstruction with Weakly-Supervised Learning From Single Image to Image...
亮点:主要是提出了通过同一人多张图来生成更精细的人脸3D模型的方案,主要框架是基于3DMM的。 Lambertian surface-朗伯面:即入射光会向所有方向均匀反射,也即漫反射的期望状态spherical harmonic lighting-球谐光照:就…...
2024/5/4 0:23:28
最新文章
- 安捷伦E4991A美国原装二手KEYSIGHT、E4990A阻抗分析仪
商品品牌:安捷伦Agilent/是德KEYSIGHT 商品型号:E4990A 商品价格:面议或电议 商品详情: Agilent E4990A阻抗分析仪,20 Hz 至 10/20/30/50/120 MHz 主要特性与技术指标 5 种频率选件;20 Hz 至 10/20/30/50/1…...
2024/5/9 2:27:10 - 梯度消失和梯度爆炸的一些处理方法
在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言,在此感激不尽。 权重和梯度的更新公式如下: w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...
2024/5/7 10:36:02 - 微信小程序的页面交互2
一、自定义属性 (1)定义: 微信小程序中的自定义属性实际上是由data-前缀加上一个自定义属性名组成。 (2)如何获取自定义属性的值? 用到target或currentTarget对象的dataset属性可以获取数据 ÿ…...
2024/5/7 13:11:16 - 微信小程序生命周期管理:从数据初始化到事件绑定
作为一个独立的应用开发平台,微信小程序提供了自己的生命周期机制,与我们熟悉的Vue.js框架有一些差异。掌握小程序生命周期的特点和使用技巧,对于开发高质量的小程序应用至关重要。深入理解和掌握小程序生命周期的使用技巧,将有助于我们构建出更加健壮和可维护的小程序应用。 小…...
2024/5/7 6:07:09 - 【外汇早评】美通胀数据走低,美元调整
原标题:【外汇早评】美通胀数据走低,美元调整昨日美国方面公布了新一期的核心PCE物价指数数据,同比增长1.6%,低于前值和预期值的1.7%,距离美联储的通胀目标2%继续走低,通胀压力较低,且此前美国一季度GDP初值中的消费部分下滑明显,因此市场对美联储后续更可能降息的政策…...
2024/5/8 6:01:22 - 【原油贵金属周评】原油多头拥挤,价格调整
原标题:【原油贵金属周评】原油多头拥挤,价格调整本周国际劳动节,我们喜迎四天假期,但是整个金融市场确实流动性充沛,大事频发,各个商品波动剧烈。美国方面,在本周四凌晨公布5月份的利率决议和新闻发布会,维持联邦基金利率在2.25%-2.50%不变,符合市场预期。同时美联储…...
2024/5/7 9:45:25 - 【外汇周评】靓丽非农不及疲软通胀影响
原标题:【外汇周评】靓丽非农不及疲软通胀影响在刚结束的周五,美国方面公布了新一期的非农就业数据,大幅好于前值和预期,新增就业重新回到20万以上。具体数据: 美国4月非农就业人口变动 26.3万人,预期 19万人,前值 19.6万人。 美国4月失业率 3.6%,预期 3.8%,前值 3…...
2024/5/4 23:54:56 - 【原油贵金属早评】库存继续增加,油价收跌
原标题:【原油贵金属早评】库存继续增加,油价收跌周三清晨公布美国当周API原油库存数据,上周原油库存增加281万桶至4.692亿桶,增幅超过预期的74.4万桶。且有消息人士称,沙特阿美据悉将于6月向亚洲炼油厂额外出售更多原油,印度炼油商预计将每日获得至多20万桶的额外原油供…...
2024/5/7 14:25:14 - 【外汇早评】日本央行会议纪要不改日元强势
原标题:【外汇早评】日本央行会议纪要不改日元强势近两日日元大幅走强与近期市场风险情绪上升,避险资金回流日元有关,也与前一段时间的美日贸易谈判给日本缓冲期,日本方面对汇率问题也避免继续贬值有关。虽然今日早间日本央行公布的利率会议纪要仍然是支持宽松政策,但这符…...
2024/5/4 23:54:56 - 【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响
原标题:【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响近日伊朗局势升温,导致市场担忧影响原油供给,油价试图反弹。此时OPEC表态稳定市场。据消息人士透露,沙特6月石油出口料将低于700万桶/日,沙特已经收到石油消费国提出的6月份扩大出口的“适度要求”,沙特将满…...
2024/5/4 23:55:05 - 【外汇早评】美欲与伊朗重谈协议
原标题:【外汇早评】美欲与伊朗重谈协议美国对伊朗的制裁遭到伊朗的抗议,昨日伊朗方面提出将部分退出伊核协议。而此行为又遭到欧洲方面对伊朗的谴责和警告,伊朗外长昨日回应称,欧洲国家履行它们的义务,伊核协议就能保证存续。据传闻伊朗的导弹已经对准了以色列和美国的航…...
2024/5/4 23:54:56 - 【原油贵金属早评】波动率飙升,市场情绪动荡
原标题:【原油贵金属早评】波动率飙升,市场情绪动荡因中美贸易谈判不安情绪影响,金融市场各资产品种出现明显的波动。随着美国与中方开启第十一轮谈判之际,美国按照既定计划向中国2000亿商品征收25%的关税,市场情绪有所平复,已经开始接受这一事实。虽然波动率-恐慌指数VI…...
2024/5/7 11:36:39 - 【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试
原标题:【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试美国和伊朗的局势继续升温,市场风险情绪上升,避险黄金有向上突破阻力的迹象。原油方面稍显平稳,近期美国和OPEC加大供给及市场需求回落的影响,伊朗局势并未推升油价走强。近期中美贸易谈判摩擦再度升级,美国对中…...
2024/5/4 23:54:56 - 【原油贵金属早评】市场情绪继续恶化,黄金上破
原标题:【原油贵金属早评】市场情绪继续恶化,黄金上破周初中国针对于美国加征关税的进行的反制措施引发市场情绪的大幅波动,人民币汇率出现大幅的贬值动能,金融市场受到非常明显的冲击。尤其是波动率起来之后,对于股市的表现尤其不安。隔夜美国股市出现明显的下行走势,这…...
2024/5/6 1:40:42 - 【外汇早评】美伊僵持,风险情绪继续升温
原标题:【外汇早评】美伊僵持,风险情绪继续升温昨日沙特两艘油轮再次发生爆炸事件,导致波斯湾局势进一步恶化,市场担忧美伊可能会出现摩擦生火,避险品种获得支撑,黄金和日元大幅走强。美指受中美贸易问题影响而在低位震荡。继5月12日,四艘商船在阿联酋领海附近的阿曼湾、…...
2024/5/4 23:54:56 - 【原油贵金属早评】贸易冲突导致需求低迷,油价弱势
原标题:【原油贵金属早评】贸易冲突导致需求低迷,油价弱势近日虽然伊朗局势升温,中东地区几起油船被袭击事件影响,但油价并未走高,而是出于调整结构中。由于市场预期局势失控的可能性较低,而中美贸易问题导致的全球经济衰退风险更大,需求会持续低迷,因此油价调整压力较…...
2024/5/8 20:48:49 - 氧生福地 玩美北湖(上)——为时光守候两千年
原标题:氧生福地 玩美北湖(上)——为时光守候两千年一次说走就走的旅行,只有一张高铁票的距离~ 所以,湖南郴州,我来了~ 从广州南站出发,一个半小时就到达郴州西站了。在动车上,同时改票的南风兄和我居然被分到了一个车厢,所以一路非常愉快地聊了过来。 挺好,最起…...
2024/5/7 9:26:26 - 氧生福地 玩美北湖(中)——永春梯田里的美与鲜
原标题:氧生福地 玩美北湖(中)——永春梯田里的美与鲜一觉醒来,因为大家太爱“美”照,在柳毅山庄去寻找龙女而错过了早餐时间。近十点,向导坏坏还是带着饥肠辘辘的我们去吃郴州最富有盛名的“鱼头粉”。说这是“十二分推荐”,到郴州必吃的美食之一。 哇塞!那个味美香甜…...
2024/5/4 23:54:56 - 氧生福地 玩美北湖(下)——奔跑吧骚年!
原标题:氧生福地 玩美北湖(下)——奔跑吧骚年!让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 啊……啊……啊 两…...
2024/5/8 19:33:07 - 扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!
原标题:扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!扒开伪装医用面膜,翻六倍价格宰客!当行业里的某一品项火爆了,就会有很多商家蹭热度,装逼忽悠,最近火爆朋友圈的医用面膜,被沾上了污点,到底怎么回事呢? “比普通面膜安全、效果好!痘痘、痘印、敏感肌都能用…...
2024/5/5 8:13:33 - 「发现」铁皮石斛仙草之神奇功效用于医用面膜
原标题:「发现」铁皮石斛仙草之神奇功效用于医用面膜丽彦妆铁皮石斛医用面膜|石斛多糖无菌修护补水贴19大优势: 1、铁皮石斛:自唐宋以来,一直被列为皇室贡品,铁皮石斛生于海拔1600米的悬崖峭壁之上,繁殖力差,产量极低,所以古代仅供皇室、贵族享用 2、铁皮石斛自古民间…...
2024/5/8 20:38:49 - 丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者
原标题:丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者【公司简介】 广州华彬企业隶属香港华彬集团有限公司,专注美业21年,其旗下品牌: 「圣茵美」私密荷尔蒙抗衰,产后修复 「圣仪轩」私密荷尔蒙抗衰,产后修复 「花茵莳」私密荷尔蒙抗衰,产后修复 「丽彦妆」专注医学护…...
2024/5/4 23:54:58 - 广州械字号面膜生产厂家OEM/ODM4项须知!
原标题:广州械字号面膜生产厂家OEM/ODM4项须知!广州械字号面膜生产厂家OEM/ODM流程及注意事项解读: 械字号医用面膜,其实在我国并没有严格的定义,通常我们说的医美面膜指的应该是一种「医用敷料」,也就是说,医用面膜其实算作「医疗器械」的一种,又称「医用冷敷贴」。 …...
2024/5/6 21:42:42 - 械字号医用眼膜缓解用眼过度到底有无作用?
原标题:械字号医用眼膜缓解用眼过度到底有无作用?医用眼膜/械字号眼膜/医用冷敷眼贴 凝胶层为亲水高分子材料,含70%以上的水分。体表皮肤温度传导到本产品的凝胶层,热量被凝胶内水分子吸收,通过水分的蒸发带走大量的热量,可迅速地降低体表皮肤局部温度,减轻局部皮肤的灼…...
2024/5/4 23:54:56 - 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...
解析如下:1、长按电脑电源键直至关机,然后再按一次电源健重启电脑,按F8健进入安全模式2、安全模式下进入Windows系统桌面后,按住“winR”打开运行窗口,输入“services.msc”打开服务设置3、在服务界面,选中…...
2022/11/19 21:17:18 - 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。
%读入6幅图像(每一幅图像的大小是564*564) f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...
2022/11/19 21:17:16 - 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...
win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面,在等待界面中我们需要等待操作结束才能关机,虽然这比较麻烦,但是对系统进行配置和升级…...
2022/11/19 21:17:15 - 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...
有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows,请勿关闭计算机”的提示,要过很久才能进入系统,有的用户甚至几个小时也无法进入,下面就教大家这个问题的解决方法。第一种方法:我们首先在左下角的“开始…...
2022/11/19 21:17:14 - win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...
置信有很多用户都跟小编一样遇到过这样的问题,电脑时发现开机屏幕显现“正在配置Windows Update,请勿关机”(如下图所示),而且还需求等大约5分钟才干进入系统。这是怎样回事呢?一切都是正常操作的,为什么开时机呈现“正…...
2022/11/19 21:17:13 - 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...
Win7系统开机启动时总是出现“配置Windows请勿关机”的提示,没过几秒后电脑自动重启,每次开机都这样无法进入系统,此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一:开机按下F8,在出现的Windows高级启动选…...
2022/11/19 21:17:12 - 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...
有不少windows10系统用户反映说碰到这样一个情况,就是电脑提示正在准备windows请勿关闭计算机,碰到这样的问题该怎么解决呢,现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法:1、2、依次…...
2022/11/19 21:17:11 - 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...
今天和大家分享一下win7系统重装了Win7旗舰版系统后,每次关机的时候桌面上都会显示一个“配置Windows Update的界面,提示请勿关闭计算机”,每次停留好几分钟才能正常关机,导致什么情况引起的呢?出现配置Windows Update…...
2022/11/19 21:17:10 - 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...
只能是等着,别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚,只能是考虑备份数据后重装系统了。解决来方案一:管理员运行cmd:net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...
2022/11/19 21:17:09 - 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?
原标题:电脑提示“配置Windows Update请勿关闭计算机”怎么办?win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢?一般的方…...
2022/11/19 21:17:08 - 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...
关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧!关机提示 windows7 正在配…...
2022/11/19 21:17:05 - 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...
钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...
2022/11/19 21:17:05 - 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...
前几天班里有位学生电脑(windows 7系统)出问题了,具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面,长时间没反应,无法进入系统。这个问题原来帮其他同学也解决过,网上搜了不少资料&#x…...
2022/11/19 21:17:04 - 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...
本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法,并在最后教给你1种保护系统安全的好方法,一起来看看!电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中,添加了1个新功能在“磁…...
2022/11/19 21:17:03 - 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...
许多用户在长期不使用电脑的时候,开启电脑发现电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机。。.这要怎么办呢?下面小编就带着大家一起看看吧!如果能够正常进入系统,建议您暂时移…...
2022/11/19 21:17:02 - 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...
配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧!配置windows update失败 还原更改 请勿关闭计算机&#x…...
2022/11/19 21:17:01 - 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...
不知道大家有没有遇到过这样的一个问题,就是我们的win7系统在关机的时候,总是喜欢显示“准备配置windows,请勿关机”这样的一个页面,没有什么大碍,但是如果一直等着的话就要两个小时甚至更久都关不了机,非常…...
2022/11/19 21:17:00 - 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...
当电脑出现正在准备配置windows请勿关闭计算机时,一般是您正对windows进行升级,但是这个要是长时间没有反应,我们不能再傻等下去了。可能是电脑出了别的问题了,来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...
2022/11/19 21:16:59 - 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...
我们使用电脑的过程中有时会遇到这种情况,当我们打开电脑之后,发现一直停留在一个界面:“配置Windows Update失败,还原更改请勿关闭计算机”,等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢࿰…...
2022/11/19 21:16:58 - 如何在iPhone上关闭“请勿打扰”
Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...
2022/11/19 21:16:57