【深度学习模型训练速成课】:揭秘加速技巧与性能倍增秘籍
发布时间: 2024-09-03 09:26:16 阅读量: 150 订阅数: 59
MATLAB速成技巧:Simulink基础操作与建模仿真入门
![深度学习算法优化技巧](https://img-blog.csdnimg.cn/img_convert/408596bb9278c532fa196c20fbe4cd3b.png)
# 1. 深度学习模型训练概述
深度学习已经成为了推动AI发展的关键力量,而模型训练是其中的核心环节。在这一章中,我们将介绍深度学习模型训练的基本概念,以及它在整个机器学习工作流程中的作用和重要性。
## 1.1 模型训练的基本原理
在深度学习领域,模型训练通常指的是通过优化算法对神经网络的权重进行调整的过程,直到找到最适合数据的参数。这个过程包括了前向传播(forward propagation),计算损失函数值,以及反向传播(backward propagation),利用梯度下降更新权重。
## 1.2 训练流程的重要性
一个高效的训练流程对提高模型性能至关重要。它不仅涉及数据预处理、模型选择与优化,还包括了训练策略的调整,如超参数的选择、防止过拟合的正则化技术等。训练过程的优化可以显著提高模型的准确度,并缩短训练时间。
## 1.3 训练与验证
在模型训练过程中,验证是非常关键的步骤。验证集用于评估模型在未见数据上的表现,帮助开发者了解模型的泛化能力。同时,它还有助于防止模型过拟合,并在早期发现和解决潜在的训练问题。
通过后续章节的深入讲解,我们会了解到如何在实际项目中搭建起高效的训练流程,从数据预处理、模型架构的选择,到训练过程的优化,再到模型的部署和监控,每一个环节都至关重要,需要通过细致的分析和操作来确保最佳的模型性能。
# 2. 深度学习框架与环境搭建
深度学习技术的迅猛发展,离不开高效、易用的深度学习框架。这些框架不仅大幅降低了模型开发的门槛,而且通过优化底层硬件资源的利用,加速了模型训练和推理的速度。本章将从选择深度学习框架开始,详细探讨硬件资源的选择、优化以及深度学习云服务与平台的应用。
## 2.1 选择合适的深度学习框架
深度学习框架的选择对于项目的成功至关重要。一个优秀的框架能够提供丰富的API、良好的社区支持和高效的计算性能。在众多的深度学习框架中,TensorFlow、PyTorch和Keras是最为流行的几种。
### 2.1.1 框架的功能对比
在对比不同深度学习框架时,通常会考虑以下几个方面:
- **API友好度**:API设计是否直观,易用性如何。
- **社区支持**:社区的活跃程度,文档和教程的完整性。
- **性能表现**:在特定任务上框架的运行效率。
- **模型转换**:模型是否容易迁移到其他框架或部署到生产环境。
TensorFlow由Google开发,拥有广泛的社区和大量工具,支持自动微分,便于调试和性能优化。它特别适合于大规模部署的生产环境。
PyTorch由Facebook推出,以动态计算图著称,非常适合研究与开发工作。它易于学习和使用,是许多研究人员的首选。
Keras则更注重于快速实验,提供了高层次的接口简化了深度学习模型的构建。适合快速原型开发,但性能上可能不如TensorFlow和PyTorch。
### 2.1.2 环境搭建与配置
搭建深度学习环境需要考虑以下几个步骤:
1. **选择合适的操作系统**:通常推荐使用Linux系统,因为它在资源管理、稳定性方面表现优异。
2. **安装CUDA和cuDNN**:对于需要GPU加速的深度学习项目,必须安装NVIDIA的CUDA和cuDNN库。
3. **安装深度学习框架**:可以通过Python的包管理器pip或者Anaconda环境进行安装。
4. **验证环境**:安装完成后,运行一些基本的模型来检查硬件和软件环境是否正常工作。
以TensorFlow为例,可以通过以下命令进行安装:
```bash
pip install tensorflow
```
下面展示一个简单的TensorFlow程序来验证安装:
```python
import tensorflow as tf
# 创建一个简单的TensorFlow模型
mnist = tf.keras.datasets.mnist
(x_train, y_train),(x_test, y_test) = mnist.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0
model = tf.keras.models.Sequential([
tf.keras.layers.Flatten(input_shape=(28, 28)),
tf.keras.layers.Dense(128, activation='relu'),
tf.keras.layers.Dropout(0.2),
tf.keras.layers.Dense(10, activation='softmax')
])
***pile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
model.fit(x_train, y_train, epochs=5)
model.evaluate(x_test, y_test, verbose=2)
```
此程序加载MNIST数据集,构建一个简单的卷积神经网络,并在训练集上进行训练和测试。
## 2.2 硬件资源的选择与优化
深度学习模型训练对于硬件资源的需求很高,尤其在训练大规模模型时,合理选择硬件资源并进行优化是提高效率的关键。
### 2.2.1 GPU与CPU的性能对比
GPU拥有更多的核心,能够在并行计算方面提供显著的性能提升。当处理大规模矩阵运算时,GPU比CPU表现得更为出色。而CPU在处理需要高时序逻辑的任务时,具有优势。
在深度学习的训练过程中,GPU的并行处理能力使其成为了训练的首选。然而,并非所有任务都适合GPU加速,如一些小规模数据集的训练任务,CPU就足以胜任。
### 2.2.2 优化硬件资源的配置
优化硬件资源的配置需要考虑以下几个方面:
- **多GPU训练**:对于大规模数据集,使用多块GPU进行训练可以大幅提升速度。大多数深度学习框架支持数据并行或模型并行训练。
- **内存管理**:合理规划内存使用,避免内存溢出。可以使用混合精度训练,以减少模型对内存的需求。
- **硬件监控**:使用监控工具(如NVIDIA的`nvidia-smi`)来跟踪硬件资源的使用情况,及时发现并解决问题。
## 2.3 深度学习云服务与平台
近年来,云服务平台开始扮演越来越重要的角色,在模型训练和部署方面提供了灵活高效的解决方案。
### 2.3.1 云服务平台的对比
主要的深度学习云服务平台包括Amazon SageMaker、Google AI Platform和Microsoft Azure ML等。
- **Amazon SageMaker**:提供全栈服务,支持数据准备、模型训练、优化、部署一体化流程。
- **Google AI Platform**:有强大的TensorFlow支持,提供灵活的计算资源选择,方便快速原型设计。
- **Microsoft Azure ML**:与Azure云服务深度整合,提供数据科学工作流管理。
### 2.3.2 使用云服务加速模型训练
使用云服务进行模型训练能够带来以下好处:
- **弹性资源**:可根据需要动态调整计算资源,无需担心硬件投资。
- **自动化管理**:服务提供商负责维护和升级计算资源,用户可以专注于模型开发。
- **易用性**:提供友好的用户界面,简化了环境搭建、模型训练和部署流程。
例如,在Amazon SageMaker上训练一个模型可能涉及以下步骤:
1. **准备数据**:上传数据到S3存储桶。
2. **创建训练任务**:定义训练任务配置,指定算法、计算资源和输入数据。
3. **监控训练过程**:实时查看日志和监控指标,确保训练按预期进行。
4. **模型部署**:训练完成后,一键部署模型到生产环境。
深度学习框架与环境的搭建是整个深度学习项目成功的关键,涉及到硬件选择、优化及云服务应用等复杂环节。通过综合考虑框架的特性、性能优化以及云服务的便捷性,可以为高效且可持续发展的深度学习项目打下坚实的基础。接下来章节将探讨如何在数据预处理阶段为模型训练和优化做好充分准备。
# 3. 数据预处理与增强
## 3.1 数据清洗与预处理技巧
### 数据集的标准化与归一化
在深度学习中,标准化和归一化是数据预处理的常见手段,它们能够确保输入数据的分布合理,有利于模型的训练。标准化(Standardization)是将数据按比例缩放,使之落入一个小的特定区间,通常使用z-score标准化,即减去均值后除以标准差,公式为:
\[ z = \frac{(x - \mu)}{\sigma} \]
其中,\( x \)是原始数据,\( \mu \)是数据的均值,\( \sigma \)是数据的标准差。通过这种方式,标准化后的数据其均值为0,标准差为1。
归一化(Normalization),则是将数据按比例缩放到一个确定的范围,通常是[0, 1]区间。这可以通过如下公式实现:
\[ x_{norm} = \frac{(x - x_{min})}{(x_{max} - x_{min})} \]
其中,\( x_{min} \)和\( x_{max} \)分别是特征\( x \)的最小和最大值。这步预处理尤其适用于神经网络输入层和特征缩放。
### 缺失值处理与数据转换
缺失值是数据集中常见的问题。处理缺失值的方式有很多,可以根据业务需求和数据特点来选择合适的方法。
1. 删除法:如果数据集中缺失值较少,可以直接删除这些数据,但可能会导致信息丢失。
2. 填充法:用平均值、中位数或众数来填充缺失值。对数值型数据使用平均值或中位数,对分类数据使用众数。
3. 插值法:如线性插值、多项式插值等,适用于时间序列数据。
4. 使用模型预测:构建一个回归模型来预测缺失值。
数据转换包括对数据特征进行转换,如对数转换、平方根转换、Box-Cox转换等,目的是使数据分布更接近正态分布,有助于稳定方差,减少预测误差。
```python
from sklearn.preprocessing import StandardScaler, MinMaxScaler
# 示例:对数据集进行标准化和归一化处理
import numpy as np
# 假设data是原始数据集
data = np.array([...])
# 标准化处理
scaler = StandardScaler()
data_standardized = scaler.fit_transform(data)
# 归一化处理
min_max_scaler = MinMaxScaler()
data_normalized = min_max_scaler.fit_transform(data)
```
在执行以上数据预处理操作时,需要注意的是,标准差和均值、最小值和最大值等参数需要根据训练集进行计算,然后应用到验证集和测试集上,以确保模型在预测时面对的是相同的数据分布。
## 3.2 数据增强的方法与实践
### 图像数据增强技术
图像数据增强是提高模型泛化能力的重要手段,尤其在图像分类、目标检测等领域。主要的数据增强方法有:
1. 翻转:水平或垂直翻转图像。
2. 旋转:旋转图像一个特定角度。
3. 缩放:随机缩放图像大小。
4. 剪裁:随机剪裁图像的一部分。
5. 颜色调整:改变亮度、对比度、饱和度等。
通过这些方法可以人为地扩展数据集,让模型在训练时看到更多样化的数据。
```python
from imgaug import augmenters as iaa
# 图像数据增强的示例
seq = iaa.Sequential([
iaa.Fliplr(0.5), # 水平翻转
iaa.Flipud(0.2), # 垂直翻转
iaa.Affine(
scale={"x": (0.8, 1.2), "y": (0.8, 1.2)},
translate_percent={"x": (-0.2, 0.2), "y": (-0.2, 0.2)}
) # 缩放与平移
])
# 对图像img应用上述增强序列
augmented = seq.augment_images(img)
```
### 文本与序列数据增强策略
文本和序列数据的增强不如图像数据直观,但仍有一些实用的技术:
1. 同义词替换:使用同义词库替换文本中的词汇。
2. 回译法:将文本翻译成另一种语言后再翻译回原语言。
3. 拼写错误模拟:对文本故意引入拼写错误。
4. 删除、插入、交换字符:在序列中对字符进行操作。
在NLP任务中,这些技术可以用来扩充语料库,提高模型的鲁棒性。
## 3.3 处理不平衡数据集
### 重采样技术
在处理不平衡数据集时,重采样是一种常用的方法。重采样包括两种策略:过采样和欠采样。
过采样(Oversampling)是增加少数类样本数量的方法,使类别分布更加均衡。最简单的过采样方法是重复采样少数类样本,但容易导致过拟合。SMOTE(Synthetic Minority Over-sampling Technique)是一种更为复杂的过采样方法,它通过插值合成新的少数类样本。
欠采样(Undersampling)则是减少多数类样本的数量,但可能会丢失重要信息。欠采样时可以考虑使用 Tomek Links 或 ENN(Edited Nearest Neighbors)等方法。
### 使用生成对抗网络处理不平衡数据
生成对抗网络(GAN)是一个由生成器和判别器组成的深度学习模型,可以用来生成新的样本数据。在不平衡数据集中,可以使用GAN来生成缺失的少数类数据。生成的样本应具有与真实数据相似的分布特性。
```python
# GAN用于数据增强的简要伪代码
generator = build_generator(...)
discriminator = build_discriminator(...)
# GAN的训练
for epoch in range(num_epochs):
for real_data in real_data_loader:
noise = generate_noise(...)
fake_data = generator(noise)
# 训练判别器
discriminator_loss_real = ...
discriminator_loss_fake = ...
# 训练生成器
generated_noise = ...
generator_loss = ...
# 更新模型参数
...
```
生成器在训练中不断学习生成更为真实的少数类样本,而判别器则努力区分真实数据与生成数据。最终,生成器能够生成可用的少数类样本以增强数据集。
# 4. 模型训练与调优策略
## 4.1 模型架构的选择与优化
### 模型结构的创新方法
随着深度学习技术的不断进步,模型结构的创新已成为推动这一领域发展的重要因素。从传统的全连接网络到卷积神经网络(CNN),再到循环神经网络(RNN)和更复杂的变种,如长短期记忆网络(LSTM)和Transformer,这些创新极大地提升了模型在各种任务上的表现。本节将详细探讨如何选择和优化模型架构。
在实际应用中,选择合适的模型结构需要考虑以下几个关键因素:
- 任务类型:不同的任务,如图像识别、自然语言处理(NLP)或是序列预测,对模型结构有着不同的要求。例如,图像任务通常需要卷积层来提取空间特征,而NLP任务则依赖于能够处理序列数据的RNN或Transformer结构。
- 数据规模:大规模数据集可以充分利用复杂的模型结构,如深层网络,以捕获更细微的数据特征。然而,复杂模型通常伴随着过拟合的风险,需要通过正则化技术来缓解。
- 训练资源:资源限制可能影响模型架构的选择。例如,在资源受限的环境中,可能需要采用轻量级或压缩的模型来减少计算和存储开销。
**模型结构创新的最新趋势包括:**
- 残差网络(ResNet)的跳跃连接允许梯度直接流过网络,使得更深的网络结构成为可能。
- 生成对抗网络(GAN)的引入,为生成模型提供了新的训练方式。
- Transformer模型则通过自注意力机制革新了NLP领域,BERT和GPT系列模型的出现证明了其在多种NLP任务上的卓越性能。
模型创新的另一个重要方面是神经架构搜索(NAS),通过自动化的方式搜索最优的网络结构。NAS通过算法优化,能够在给定的资源约束下,找到满足特定任务需求的最优模型结构。
### 超参数的选择与优化
超参数是影响模型性能的重要因素,它们定义了学习过程的框架,如学习率、批量大小、优化器类型、损失函数以及层数等。正确地设置这些超参数对模型的收敛速度和最终性能有着显著的影响。
超参数优化的常见方法包括:
- **网格搜索**:系统地遍历可能的超参数组合,评估每一种组合的模型性能。虽然方法直观,但计算成本高,尤其是对于超参数空间大的情况。
- **随机搜索**:随机地从预定义的分布中选择超参数值。该方法在处理大规模超参数空间时比网格搜索更高效。
- **贝叶斯优化**:使用贝叶斯原理迭代地选择超参数组合,以期找到最优解。这种方法在模型训练成本很高时尤为有效。
- **基于梯度的优化方法**:当超参数对模型性能的影响可微时,可以使用梯度下降的方法来调整超参数。
在实践操作中,一个有效的策略是首先使用较小的样本集合进行快速的网格搜索或随机搜索,以缩小超参数的搜索范围,然后再使用更精细的搜索方法,如贝叶斯优化,来找到最佳配置。另外,也可以利用现有的经验公式作为起点,然后在此基础上进行微调。
```python
# 示例代码:使用网格搜索进行超参数优化
from sklearn.model_selection import GridSearchCV
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_digits
# 加载数据集
digits = load_digits()
X, y = digits.data, digits.target
# 设置超参数的网格范围
param_grid = {
'n_estimators': [10, 50, 100, 200],
'max_depth': [None, 5, 10, 15],
'min_samples_split': [2, 5, 10]
}
# 创建随机森林模型实例
rf = RandomForestClassifier()
# 初始化网格搜索
grid_search = GridSearchCV(estimator=rf, param_grid=param_grid, cv=5)
# 执行网格搜索
grid_search.fit(X, y)
# 输出最佳参数组合
print("Best parameters found: ", grid_search.best_params_)
```
在上述代码中,我们使用了`GridSearchCV`来对随机森林分类器的三个超参数进行网格搜索。代码中的`param_grid`定义了要搜索的参数范围,`cv`参数设置了交叉验证的折数。网格搜索完成后,`best_params_`属性包含了找到的最佳参数组合。需要注意的是,实际应用中应根据具体问题适当选择超参数范围和值。
# 5. 分布式训练与性能提升
分布式训练已经成为深度学习领域的关键技术,尤其是随着模型规模的不断扩大,单机资源已难以满足训练需求。分布式训练不仅可以将大规模模型分布在多个计算节点上,还可以通过并行计算缩短训练时间,提升训练效率。
## 5.1 分布式训练的基本原理
在深入探讨分布式训练的具体实践之前,我们需要了解其背后的基本原理。分布式训练的核心在于如何有效地将数据和模型分布在多个设备上,以及如何同步更新模型参数来加速训练过程。
### 5.1.1 数据并行与模型并行
数据并行是将数据集分成多个批次,每个计算节点处理一个批次的数据,并且维护全局模型的一份副本。在训练过程中,各节点并行计算梯度,并将梯度汇总到一个中心节点来更新全局模型。
模型并行则是将模型分割成多个部分,每个部分被放置在不同的计算节点上。由于模型并行需要频繁的节点间通信,其设计和实现比数据并行复杂得多。
### 5.1.2 同步与异步训练机制
同步训练机制中,所有计算节点在更新全局模型之前需要等待彼此完成梯度计算,确保模型参数同步更新。这种机制保证了模型更新的一致性,但可能会因为网络延迟或计算速度不一致导致效率低下。
异步训练允许计算节点不必等待其他节点完成就更新全局模型,这提高了资源的利用率和系统的吞吐量。但是,异步训练可能会导致模型更新出现冲突和延迟,影响最终模型的收敛速度和质量。
## 5.2 实践分布式训练技术
在实际应用中,分布式训练技术的选择、部署和优化是提升模型训练性能的关键步骤。
### 5.2.1 分布式训练的框架选择
目前有多种分布式训练框架可供选择,如TensorFlow、PyTorch、Horovod等。不同框架各有优势,例如Horovod是Uber推出的一个开源框架,它通过封装MPI(Message Passing Interface)简化了多GPU或多节点训练过程。
### 5.2.2 分布式训练的部署与优化
分布式训练的部署要考虑到节点间的网络拓扑结构、通信带宽和延迟等实际因素。为了优化性能,需要对计算资源进行合理分配,比如合理设置批处理大小,选择合适的优化器和学习率策略。
代码示例1:使用Horovod进行分布式训练的基本流程。
```python
import horovod.tensorflow as hvd
# 初始化Horovod
hvd.init()
# 指定使用GPU
gpus = tf.config.experimental.list_physical_devices('GPU')
for gpu in gpus:
tf.config.experimental.set_memory_growth(gpu, True)
if gpus:
tf.config.experimental.set_visible_devices(gpus[hvd.local_rank()], 'GPU')
# 构建模型、损失函数和优化器
model = ...
loss_fn = ...
optimizer = ...
# 使用Horovod分布式优化器
optimizer = hvd.DistributedOptimizer(optimizer, named_parameters=model.parameters())
# Horovod: broadcast parameters & initial states from rank=0 to all other processes.
hooks = [hvd.BroadcastGlobalVariablesHook(0)]
# 模型训练
for epoch in range(epochs):
for batch in dataset:
# 计算梯度、更新参数等
with tf.GradientTape() as tape:
loss_value = loss_fn(batch)
grads = tape.gradient(loss_value, model.trainable_variables)
optimizer.apply_gradients(zip(grads, model.trainable_variables))
# 在所有进程中同步模型状态
hvd.join()
```
在上述代码中,通过初始化Horovod(行2-4),指定使用GPU(行10-13),并且使用`DistributedOptimizer`(行17)来替换常规的优化器。在训练循环中,通过`hvd.join()`(行32)来同步所有进程的状态。
## 5.3 性能监控与故障排除
为了确保分布式训练的顺利进行,性能监控和故障排除是不可或缺的环节。
### 5.3.1 性能监控工具与指标
性能监控工具如TensorBoard、Ganglia、Prometheus等可以帮助我们观察训练过程中的资源使用情况和性能指标。监控指标包括但不限于训练速度、损失函数值、梯度范数、学习率等。
### 5.3.2 常见故障的诊断与解决方法
在分布式训练中,常见的问题包括节点通信失败、硬件故障、软件冲突等。要解决这些问题,首先需要快速定位问题所在,比如通过日志分析、网络诊断工具等。一旦定位问题,就需要根据具体情况进行故障排除,可能的解决方法包括但不限于重启服务、调整网络配置、修改代码等。
通过本章的介绍,我们可以看到分布式训练在深度学习中的重要性以及实现细节。从原理的掌握到实践中的部署与优化,再到性能监控与故障排除,分布式训练不仅需要扎实的技术功底,还需要丰富的实践经验。随着深度学习技术的不断发展,分布式训练将继续成为推动AI发展的关键技术之一。
# 6. 模型部署与应用优化
## 6.1 模型压缩与量化技术
在深度学习模型的部署过程中,模型压缩和量化是两个关键的技术手段,它们可以帮助我们减少模型的大小,提高推理速度,减少资源消耗,并且有时候能够保持模型性能不减。
### 6.1.1 参数剪枝与稀疏化
参数剪枝是一种通过去除模型中不重要的权重来减少模型大小和计算需求的方法。剪枝可以基于多种标准进行,例如权重的绝对值大小、权重梯度的大小或基于特定的剪枝算法。
```python
import torch
import torch.nn.utils.prune as prune
# 假设我们有一个预训练的模型
model = ...
# 一个简单的剪枝策略:剪掉权重绝对值小于某个阈值的参数
prune.global_unstructured(
model.parameters(),
pruning_method=prune.RandomUnstructured,
amount=0.1, # 保留90%的权重
)
```
### 6.1.2 模型量化的方法与优势
量化是一种将模型参数从浮点数转换为低精度表示(如8位整数)的技术。这种方法可以大大减少模型的存储和运行时需求,同时利用特定硬件上的加速器。
```python
# 使用PyTorch的量化功能
model = ...
# 转换模型为8位整数量化模型
quantized_model = torch.quantization.quantize_dynamic(
model, # the original model
{torch.nn.Linear}, # a set of layers to dynamically quantize
dtype=torch.qint8 # the target dtype for quantized weights
)
```
## 6.2 模型部署的最佳实践
当模型准备好部署到生产环境时,选择正确的平台和遵循最佳实践可以显著影响模型性能和可用性。
### 6.2.1 选择合适的模型部署平台
在选择模型部署平台时,我们需要考虑模型的特性、部署环境以及预期的流量。例如,TensorFlow Serving适合于大规模的、需要频繁更新的模型部署。而如果环境更加轻量级,像TorchServe或ONNX Runtime可能更合适。
### 6.2.2 部署流程与优化策略
模型部署流程通常包括模型导出、服务器配置、API接口设计等步骤。在模型部署时,可以通过调整批处理大小、使用异步处理和负载均衡等技术来优化性能。
```mermaid
graph TD
A[模型训练] --> B[模型导出]
B --> C[选择部署平台]
C --> D[服务器配置]
D --> E[API接口设计]
E --> F[部署模型]
F --> G[性能监控]
G --> H[模型优化]
```
## 6.3 模型在生产环境中的优化
部署到生产环境后,持续监控模型性能并根据需要进行调整是至关重要的。
### 6.3.1 实时性能监控与调优
监控模型的延迟、吞吐量、资源使用情况等指标,可以帮助我们及时发现和解决问题。实现这一目标可以使用Prometheus、Grafana这样的开源监控工具。
### 6.3.2 模型服务化与API设计
良好的API设计可以让模型更易于集成和维护。RESTful API或gRPC是两种常见的模型服务化方式。设计API时,需考虑安全性、版本管理和请求负载。
在实施上述策略的同时,确保模型的安全性和数据隐私是不可忽视的。加密技术如SSL/TLS和API密钥可以提供安全保护,而合规的数据处理流程则保障数据隐私。
在本章中,我们探讨了模型部署的不同方面,从压缩和量化到部署流程和生产环境中的优化。这些知识对于确保深度学习模型能够顺利转化为实际可用的应用至关重要。
0
0