IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 人工智能 -> 余弦退火学习率调整代码 -> 正文阅读

[人工智能]余弦退火学习率调整代码

(一)keras学习率余弦退火CosineAnnealing

https://blog.csdn.net/CarryLvan/article/details/104394960

keras学习率余弦退火CosineAnnealing
1.引言
2.余弦退火的原理
3.keras实现
1.引言
当我们使用梯度下降算法来优化目标函数的时候,当越来越接近Loss值的全局最小值时,学习率应该变得更小来使得模型不会超调且尽可能接近这一点,而余弦退火(Cosine annealing)可以通过余弦函数来降低学习率。余弦函数中随着x的增加余弦值首先缓慢下降,然后加速下降,再次缓慢下降。这种下降模式能和学习率配合,以一种十分有效的计算方式来产生很好的效果。


在论文Stochastic Gradient Descent with Warm Restarts中介绍主要介绍了带重启的随机梯度下降算法(SGDR),其中就引入了余弦退火的学习率下降方式,本文主要介绍余弦退火的原理以及实现。并且因为我们的目标优化函数可能是多峰的(如下图所示),除了全局最优解之外还有多个局部最优解,在训练时梯度下降算法可能陷入局部最小值,此时可以通过突然提高学习率,来“跳出”局部最小值并找到通向全局最小值的路径。这种方式称为带重启的随机梯度下降方法。


2.余弦退火的原理
论文介绍最简单的热重启的方法。当执行完T i T_iT?
i
?
?个epoch之后就会开始热重启(warm restart),而下标i ii就是指的第几次restart,其中重启并不是重头开始,而是通过增加学习率来模拟,并且重启之后使用旧的x t x_tx?
t
?
?作为初始解,这里的x t x_tx?
t
?
?就是通过梯度下降求解loss函数的解,也就是神经网络中的权重,因为重启就是为了通过增大学习率来跳过局部最优,所以需要将x t x_tx?
t
?
?置为旧值。

本文并不涉及重启部分的内容,所以只考虑在每一次run(包含重启就是restart)中,学习率是如何减小的。余弦退火( cosine annealing )的原理如下:
η t = η m i n i + 1 2 ( η m a x i ? η m i n i ) ( 1 + c o s ( T c u r T i π ) ) \eta_t=\eta_{min}^{i}+\frac{1}{2}(\eta_{max}^{i}-\eta_{min}^{i})(1+cos(\frac{T_{cur}}{T_i}\pi))
η?
t
?
?=η?
min
i
?
?+?
2
1
?
?(η?
max
i
?
??η?
min
i
?
?)(1+cos(?
T?
i
?
?
T?
cur
?
?
?
?π))

表达式中的字符含义:

i ii就是第几次run(索引值);
η m a x i \eta_{max}^{i}η?
max
i
?
?和η m i n i \eta_{min}^{i}η?
min
i
?
?分别表示学习率的最大值和最小值,定义了学习率的范围。论文中提到在每次restart之后,减少η m a x i \eta_{max}^{i}η?
max
i
?
?和η m i n i \eta_{min}^{i}η?
min
i
?
?的值会是有趣的,但是为了简单,论文中也保持η m a x i \eta_{max}^{i}η?
max
i
?
?和η m i n i \eta_{min}^{i}η?
min
i
?
?在每次restart之后仍然保持不变。
T c u r T_{cur}T?
cur
?
?则表示当前执行了多少个epoch,但是T c u r T_{cur}T?
cur
?
?是在每个batch运行之后就会更新,而此时一个epoch还没有执行完,所以T c u r T_{cur}T?
cur
?
?的值可以为小数。例如总样本为80,每个batch的大小是16,那么在一个epoch中就会循环5次读入batch,那么在第一个epoch中执行完第一个batch后,T c u r T_{cur}T?
cur
?
?的值就更新为1/5=0.2,以此类推。
T i T_iT?
i
?
?表示第i次run中总的epoch数。当涉及到重启时,论文中提到为了提高性能表现,开始会初始化一个比较小的T i T_iT?
i
?
?,在每次restart后,T i T_iT?
i
?
?会以乘以一个T m u l t T_{mult}T?
mult
?
?的方式增加,但是本文不涉及重启也就不需要考虑,即把T i T_iT?
i
?
?固定为我们训练模型的epoch数。
3.keras实现
为了简单,这里稍微修改一下T c u r T_{cur}T?
cur
?
?和T i T_{i}T?
i
?
?的定义,原本表示的是epoch的数量,但是因为T c u r T_{cur}T?
cur
?
?是在每个batch之后都会更新,所以将T i T_{i}T?
i
?
?定义为总的batch需要执行的步数,而T c u r T_{cur}T?
cur
?
?定义为当前对当前已经执行的batch的计数,即每执行一个batch,T c u r T_{cur}T?
cur
?
?就加一。举个例子,样本总数为80,每个batch的大小为16,那么一共有5个batch,再令训练模型总的epoch为30,假设当前执行到第二个epoch的第二个batch结束,那么此时T c u r / T i = ( 1 ? 5 + 2 ) / ( 30 ? 5 ) T_{cur}/T_i=(1*5+2)/(30*5)T?
cur
?
?/T?
i
?
?=(1?5+2)/(30?5),按照之前的定义T c u r / T i = ( 1 + 2 / 5 ) / 30 T_{cur}/T_i=(1+2/5)/30T?
cur
?
?/T?
i
?
?=(1+2/5)/30,两者是等价的,但是因为之前的定义存在小数,如果1除以batch的总数除不尽,就会存在精度损失的情况。

这里除了实现余弦退火之外,还加入了warm up预热阶段,在warm up阶段学习率线性增长,当达到我们设置的学习率之后,再通过余弦退火的方式降低学习率。

为什么使用Warmup?
由于刚开始训练时,模型的权重(weights)是随机初始化的,此时若选择一个较大的学习率,可能带来模型的不稳定(振荡),选择Warmup预热学习率的方式,可以使得开始训练的几个epoches或者一些steps内学习率较小,在预热的小学习率下,模型可以慢慢趋于稳定,等模型相对稳定后再选择预先设置的学习率进行训练,使得模型收敛速度变得更快,模型效果更佳。

keras通过继承Callback实现余弦退火。通过继承Callback,当我们训练的时候传入我们的就函数,就可以在每个batch开始训练前以及结束后回调我们重写的on_batch_end和on_batch_begin函数。

完整代码(源自github):

import numpy as np
from tensorflow import keras
from keras import backend as K


def cosine_decay_with_warmup(global_step,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ?learning_rate_base,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ?total_steps,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ?warmup_learning_rate=0.0,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ?warmup_steps=0,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ?hold_base_rate_steps=0):
? ?"""
? ?参数:
? ??? ??? ?global_step: 上面定义的Tcur,记录当前执行的步数。
? ??? ??? ?learning_rate_base:预先设置的学习率,当warm_up阶段学习率增加到learning_rate_base,就开始学习率下降。
? ??? ??? ?total_steps: 是总的训练的步数,等于epoch*sample_count/batch_size,(sample_count是样本总数,epoch是总的循环次数)
? ??? ??? ?warmup_learning_rate: 这是warm up阶段线性增长的初始值
? ??? ??? ?warmup_steps: warm_up总的需要持续的步数
? ??? ??? ?hold_base_rate_steps: 这是可选的参数,即当warm up阶段结束后保持学习率不变,知道hold_base_rate_steps结束后才开始学习率下降
? ?"""
? ? if total_steps < warmup_steps:
? ? ? ? raise ValueError('total_steps must be larger or equal to '
? ? ? ? ? ? ? ? ? ? ? ? ?'warmup_steps.')
? ? #这里实现了余弦退火的原理,设置学习率的最小值为0,所以简化了表达式
? ? learning_rate = 0.5 * learning_rate_base * (1 + np.cos(np.pi *
? ? ? ? (global_step - warmup_steps - hold_base_rate_steps) / float(total_steps - warmup_steps - hold_base_rate_steps)))
? ? #如果hold_base_rate_steps大于0,表明在warm up结束后学习率在一定步数内保持不变
? ? if hold_base_rate_steps > 0:
? ? ? ? learning_rate = np.where(global_step > warmup_steps + hold_base_rate_steps,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?learning_rate, learning_rate_base)
? ? if warmup_steps > 0:
? ? ? ? if learning_rate_base < warmup_learning_rate:
? ? ? ? ? ? raise ValueError('learning_rate_base must be larger or equal to '
? ? ? ? ? ? ? ? ? ? ? ? ? ? ?'warmup_learning_rate.')
? ? ? ? #线性增长的实现
? ? ? ? slope = (learning_rate_base - warmup_learning_rate) / warmup_steps
? ? ? ? warmup_rate = slope * global_step + warmup_learning_rate
? ? ? ? #只有当global_step 仍然处于warm up阶段才会使用线性增长的学习率warmup_rate,否则使用余弦退火的学习率learning_rate
? ? ? ? learning_rate = np.where(global_step < warmup_steps, warmup_rate,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?learning_rate)
? ? return np.where(global_step > total_steps, 0.0, learning_rate)


class WarmUpCosineDecayScheduler(keras.callbacks.Callback):
? ? """
? ? 继承Callback,实现对学习率的调度
? ? """
? ? def __init__(self,
? ? ? ? ? ? ? ? ?learning_rate_base,
? ? ? ? ? ? ? ? ?total_steps,
? ? ? ? ? ? ? ? ?global_step_init=0,
? ? ? ? ? ? ? ? ?warmup_learning_rate=0.0,
? ? ? ? ? ? ? ? ?warmup_steps=0,
? ? ? ? ? ? ? ? ?hold_base_rate_steps=0,
? ? ? ? ? ? ? ? ?verbose=0):
? ? ? ? super(WarmUpCosineDecayScheduler, self).__init__()
? ? ? ? self.learning_rate_base = learning_rate_base
? ? ? ? self.total_steps = total_steps
? ? ? ? self.global_step = global_step_init
? ? ? ? self.warmup_learning_rate = warmup_learning_rate
? ? ? ? self.warmup_steps = warmup_steps
? ? ? ? self.hold_base_rate_steps = hold_base_rate_steps
? ? ? ? self.verbose = verbose
? ? ? ? #learning_rates用于记录每次更新后的学习率,方便图形化观察
? ? ? ? self.learning_rates = []
?? ?#更新global_step,并记录当前学习率
? ? def on_batch_end(self, batch, logs=None):
? ? ? ? self.global_step = self.global_step + 1
? ? ? ? lr = K.get_value(self.model.optimizer.lr)
? ? ? ? self.learning_rates.append(lr)
?? ?#更新学习率
? ? def on_batch_begin(self, batch, logs=None):
? ? ? ? lr = cosine_decay_with_warmup(global_step=self.global_step,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? learning_rate_base=self.learning_rate_base,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? total_steps=self.total_steps,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? warmup_learning_rate=self.warmup_learning_rate,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? warmup_steps=self.warmup_steps,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? hold_base_rate_steps=self.hold_base_rate_steps)
? ? ? ? K.set_value(self.model.optimizer.lr, lr)
? ? ? ? if self.verbose > 0:
? ? ? ? ? ? print('\nBatch %05d: setting learning '
? ? ? ? ? ? ? ? ? 'rate to %s.' % (self.global_step + 1, lr))

下面的代码构建了一个简单的模型,并使用了warm up和余弦退火的方式来规划学习率。

from keras.models import Sequential
from keras.layers import Dense
# Create a model.
model = Sequential()
model.add(Dense(32, activation='relu', input_dim=100))
model.add(Dense(10, activation='softmax'))
model.compile(optimizer='rmsprop',
? ? ? ? ? ? ? ? loss='categorical_crossentropy',
? ? ? ? ? ? ? ? metrics=['accuracy'])

#样本总数
sample_count = 12608
# Total epochs to train.
epochs = 50
# Number of warmup epochs.
warmup_epoch = 10
# Training batch size, set small value here for demonstration purpose.
batch_size = 16
# Base learning rate after warmup.
learning_rate_base = 0.0001

total_steps = int(epochs * sample_count / batch_size)

# Compute the number of warmup batches.
warmup_steps = int(warmup_epoch * sample_count / batch_size)

# Generate dummy data.
data = np.random.random((sample_count, 100))
labels = np.random.randint(10, size=(sample_count, 1))

# Convert labels to categorical one-hot encoding.
one_hot_labels = keras.utils.to_categorical(labels, num_classes=10)

# Compute the number of warmup batches.
warmup_batches = warmup_epoch * sample_count / batch_size

# Create the Learning rate scheduler.
warm_up_lr = WarmUpCosineDecayScheduler(learning_rate_base=learning_rate_base,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? total_steps=total_steps,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? warmup_learning_rate=4e-06,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? warmup_steps=warmup_steps,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? hold_base_rate_steps=5,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? )

# Train the model, iterating on the data in batches of 32 samples
model.fit(data, one_hot_labels, epochs=epochs, batch_size=batch_size,
? ? ? ? ? ? verbose=0, callbacks=[warm_up_lr])

import matplotlib.pyplot as plt
plt.plot(warm_up_lr.learning_rates)
plt.xlabel('Step', fontsize=20)
plt.ylabel('lr', fontsize=20)
plt.axis([0, total_steps, 0, learning_rate_base*1.1])
plt.xticks(np.arange(0, epochs, 1))
plt.grid()
plt.title('Cosine decay with warmup', fontsize=20)
plt.show()

运行结果:


参考博客:
称霸Kaggle的十大深度学习技巧
学习率规划-余弦退火CosineAnnealing和WarmRestart原理及实现
Warmup预热学习率
参考论文:
https://arxiv.org/abs/1608.03983
————————————————
版权声明:本文为CSDN博主「Donreen」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/CarryLvan/article/details/104394960

(二)深度学习:学习率规划-余弦退火CosineAnnealing和WarmRestart原理及实现

https://blog.csdn.net/qq_38290475/article/details/103548680

摘要:文献【1】中除了权重衰减还利用了余弦退火(Cosine Annealing)以及Warm Restart,本文介绍这两种方法的原理及numpy和Keras的实现方法,其中Keras实现中继承回调函数Callbacks。

目录:
原理介绍
Numpy直观实现
Keras实现
主要参考文献:
【1】DECOUPLED WEIGHT DECAY REGULARIZATION

1. 原理介绍
论文中对学习率规划原理描述如下,公式(5)表明学习率随迭代次数的变化规律。

变量名称解释如下:

各上标、下标i ii为run的序号,意思是第i ii次的restart。

η m i n i \eta^i_{min}η?
min
i
?
?以及η m a x i \eta^i_{max}η?
max
i
?
?为学习率的范围。

T c u r T_{cur}T?
cur
?
?虽然写着是计算epoch的,但后面一句又说是随着iteration变化的。

T i T_iT?
i
?
?是当前run总共的epoch数目。

对T i T_iT?
i
?
?和T c u r T_{cur}T?
cur
?
?,文中显示T c u r T_{cur}T?
cur
?
?可以是离散的小数,这里进行如下修改便于实现:

T c u r T i = T c u r × n b a t c h e s T i × n b a t c h e s = i t e r a t i o n T o t a l I t e r a t i o n s \frac{T_{cur}}{T_i}=\frac{T_{cur} \times n_{batches}}{T_i \times n_{batches}}=\frac{iteration}{TotalIterations}
T?
i
?
?
T?
cur
?
?
?
?=?
T?
i
?
?×n?
batches
?
?
T?
cur
?
?×n?
batches
?
?
?
?=?
TotalIterations
iteration
?
?

这样就变成了当前的iteration数目的计量。

具体实现时,在训练过程中,轮到相应的epoch时重新设置优化器的T o t a l I t e r a t i o n s TotalIterationsTotalIterations并初始化T c u r T_{cur}T?
cur
?
?。

下面先用numpy简单可视化一下这样操作后学习率的变化。实际使用中一般继承Tensorflow(Keras)或者Pytorch等框架自带的学习率规划类。

2. Numpy直观实现
利用Numpy可视化余弦退火和Warm Restart之后的学习率变化情况。

import numpy as np
import matplotlib.pyplot as plt

def compute_eta_t(eta_min, eta_max, T_cur, Ti):
? ? '''Equation (5).
? ? # Arguments
? ? ? ? eta_min,eta_max,T_cur,Ti are same as equation.
? ? # Returns
? ? ? ? eta_t
? ? '''
? ? pi = np.pi
? ? eta_t = eta_min + 0.5 * (eta_max - eta_min) * (np.cos(pi * T_cur / Ti) + 1)
? ? return eta_t

# 每Ti个epoch进行一次restart。
Ti = [20, 40, 80, 160]
n_batches = 200
eta_ts = []
for ti in Ti:
? ? T_cur = np.arange(0, ti, 1 / n_batches)
? ? for t_cur in T_cur:
? ? ? ? eta_ts.append(compute_eta_t(0, 1, t_cur, ti))
? ? ? ??
n_iterations = sum(Ti) * n_batches
epoch = np.arange(0, n_iterations) / n_batches

plt.plot(epoch, eta_ts)

结果如下,和【1】的附录中给出的学习率变化基本相同。


3. Keras实现
下面进行余弦退火和warm restart的框架实现。

使用Keras框架,继承Callback实现余弦退火,warm restart可以手动循环实现。。。

首先定义余弦退火类,在每个batch结束后计算eta。

from keras import *
class CosineAnnealing(callbacks.Callback):
? ? """Cosine annealing according to DECOUPLED WEIGHT DECAY REGULARIZATION.

? ? # Arguments
? ? ? ? eta_max: float, eta_max in eq(5).
? ? ? ? eta_min: float, eta_min in eq(5).
? ? ? ? total_iteration: int, Ti in eq(5).
? ? ? ? iteration: int, T_cur in eq(5).
? ? ? ? verbose: 0 or 1.
? ? """

? ? def __init__(self, eta_max=1, eta_min=0, total_iteration=0, iteration=0, verbose=0, **kwargs):
? ? ? ??
? ? ? ? super(CosineAnnealing, self).__init__()

? ? ? ? global lr_list
? ? ? ??
? ? ? ? lr_list = []
? ? ? ? self.eta_max = eta_max
? ? ? ? self.eta_min = eta_min
? ? ? ? self.verbose = verbose
? ? ? ? self.total_iteration = total_iteration
? ? ? ? self.iteration = iteration
? ??
? ? def on_train_begin(self, logs=None):
? ? ? ? self.lr = K.get_value(self.model.optimizer.lr)
? ? ? ??
? ? def on_train_end(self, logs=None):
? ? ? ? K.set_value(self.model.optimizer.lr, self.lr)
? ? ? ??

? ? def on_batch_end(self, epoch, logs=None):
? ? ? ? self.iteration += 1
? ? ? ? logs = logs or {}
? ? ? ? logs['lr'] = K.get_value(self.model.optimizer.lr)
? ? ? ??
? ? ? ? eta_t = self.eta_min + (self.eta_max - self.eta_min) * 0.5 * (1 + np.cos(np.pi * self.iteration / self.total_iteration))
? ? ? ? new_lr = self.lr * eta_t
? ? ? ? K.set_value(self.model.optimizer.lr, new_lr)
? ? ? ? if self.verbose > 0:
? ? ? ? ? ? print('\nEpoch %05d: CosineAnnealing '
? ? ? ? ? ? ? ? ? 'learning rate to %s.' % (epoch + 1, new_lr))
? ? ? ? lr_list.append(logs['lr'])

下面是数据及模型的创建。

import keras
from keras import layers
import matplotlib.pyplot as plt
import numpy as np
import tensorflow as tf

import keras.backend as K

# 准备数据集
num_train, num_test = 2000, 100
num_features = 200

true_w, true_b = np.ones((num_features, 1)) * 0.01, 0.05

features = np.random.normal(0, 1, (num_train + num_test, num_features))
noises = np.random.normal(0, 1, (num_train + num_test, 1)) * 0.01
labels = np.dot(features, true_w) + true_b + noises

train_data, test_data = features[:num_train, :], features[num_train:, :]
train_labels, test_labels = labels[:num_train], labels[num_train:]

# 选择模型
model = keras.models.Sequential([
? ? layers.Dense(units=128, activation='relu', input_dim=200),?
? ? layers.Dense(128, activation='relu', kernel_regularizer=keras.regularizers.l2(0.00)),
? ? layers.Dense(1)
])

model.summary()
model.compile(optimizer='adam',
? ? ? ? ? ? ? loss='mse',
? ? ? ? ? ? ? metrics=['mse'])

最后是训练过程,在这里加上前面定义的回调函数,并手动实现WarmRestart。

epochs = [2, 4, 8, 16, 32]
lr_lists = []
for Ti in epochs:
? ? reduce_lr = CosineAnnealing(eta_max=1, eta_min=0, total_iteration=Ti * (2000 // 16), iteration=0, verbose=0)

? ? hist1 = model.fit(train_data, train_labels, batch_size=16, epochs=Ti, validation_data=[test_data, test_labels], callbacks=[reduce_lr])
? ??
? ? lr_lists += lr_list
? ??
plt.plot(lr_lists)

最后学习率在训练过程中的变化如图所示。

————————————————
版权声明:本文为CSDN博主「Ten_yn」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_38290475/article/details/103548680

  人工智能 最新文章
2022吴恩达机器学习课程——第二课(神经网
第十五章 规则学习
FixMatch: Simplifying Semi-Supervised Le
数据挖掘Java——Kmeans算法的实现
大脑皮层的分割方法
【翻译】GPT-3是如何工作的
论文笔记:TEACHTEXT: CrossModal Generaliz
python从零学(六)
详解Python 3.x 导入(import)
【答读者问27】backtrader不支持最新版本的
上一篇文章      下一篇文章      查看所有文章
加:2021-08-14 14:01:52  更:2021-08-14 14:02:07 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/27 20:24:33-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码