• 深度学习之模型压缩、加速模型推理


    简介

    当将一个机器学习模型部署到生产环境中时,通常需要满足一些在模型原型阶段没有考虑到的要求。例如,在生产中使用的模型将不得不处理来自不同用户的大量请求。因此,您将希望进行优化,以获得较低的延迟和/或吞吐量。

    • 延迟:是任务完成所需的时间,就像单击链接后加载网页所需的时间。它是开始某项任务和看到结果之间的等待时间。 

    • 吞吐量:是系统在一定时间内可以处理的请求数。

    这意味着机器学习模型在进行预测时必须非常快速,为此有各种技术可以提高模型推断的速度,本文将介绍其中最重要的一些。

    模型压缩

    有一些旨在使模型更小的技术,因此它们被称为模型压缩技术,而另一些则侧重于使模型在推断阶段更快,因此属于模型优化领域。但通常使模型更小也有助于提高推断速度,因此在这两个研究领域之间的界限非常模糊。

    低秩分解

    这是我们首次看到的第一种方法,它正在受到广泛研究,事实上,最近已经有很多关于它的论文发布。

    基本思想是用低维度的矩阵(虽然更正确的说法是张量,因为我们经常有超过2维的矩阵)替换神经网络的矩阵(表示网络层的矩阵)。通过这种方式,我们将减少网络参数的数量,从而提高推断速度。

    一个微不足道的例子是,在CNN网络中,将3x3的卷积替换为1x1的卷积。这种技术被用于网络结构中,比如SqueezeNet。

    最近,类似的思想也被应用于其他用途,比如允许在资源有限的情况下微调大型语言模型。当为下游任务微调预训练模型时,仍然需要在预训练模型的所有参数上训练模型,这可能非常昂贵。

    因此,名为“大型语言模型的低秩适应”(或LoRA)的方法的思想是用较小的矩阵对原始模型进行替换(使用矩阵分解),这些矩阵具有较小的尺寸。这样,只需要重新训练这些新矩阵,以使预训练模型适应更多下游任务。

    a93b001d4553c55f2d3828dad2627218.png

    在LoRA中的矩阵分解

    现在,让我们看看如何使用Hugging Face的PEFT库来实现对LoRA进行微调。假设我们想要使用LoRA对bigscience/mt0-large进行微调。首先,我们必须确保导入我们需要的内容。

    1. !pip install peft
    2. !pip install transformers
    1. from transformers import AutoModelForSeq2SeqLM
    2. from peft import get_peft_model, LoraConfig, TaskType
    3. model_name_or_path = "bigscience/mt0-large"
    4. tokenizer_name_or_path = "bigscience/mt0-large"

    接下来的步骤将是创建在微调期间应用于LoRA的配置。

    1. peft_config = LoraConfig(
    2. task_type=TaskType.SEQ_2_SEQ_LM, inference_mode=False, r=8, lora_alpha=32, lora_dropout=0.1
    3. )

    然后,我们使用Transformers库的基本模型以及我们为LoRA创建的配置对象来实例化模型。

    1. model = AutoModelForSeq2SeqLM.from_pretrained(model_name_or_path)
    2. model = get_peft_model(model, peft_config)
    3. model.print_trainable_parameters()

    知识蒸馏

    这是另一种方法,允许我们将“小”模型放入生产中。思想是有一个称为教师的大模型,和一个称为学生的较小模型,我们将使用教师的知识来教学生如何进行预测。这样,我们可以只将学生放入生产环境中。

    68c46b7a8164d1b48e40fa67a561a472.png

    这种方法的一个经典示例是以这种方式开发的模型DistillBERT,它是BERT的学生模型。DistilBERT比BERT小40%,但保留了97%的语言理解能力,并且推断速度快60%。这种方法有一个缺点是:您仍然需要拥有大型教师模型,以便对学生进行训练,而您可能没有足够的资源来训练类似教师的模型。

    让我们看看如何在Python中进行知识蒸馏的简单示例。要理解的一个关键概念是Kullback–Leibler散度,它是一个用于理解两个分布之间差异的数学概念,实际上在我们的案例中,我们想要理解两个模型的预测之间的差异,因此训练的损失函数将基于这个数学概念。

    1. import tensorflow as tf
    2. from tensorflow.keras import layers, models
    3. from tensorflow.keras.datasets import mnist
    4. from tensorflow.keras.utils import to_categorical
    5. import numpy as np
    6. # Load the MNIST dataset
    7. (train_images, train_labels), (test_images, test_labels) = mnist.load_data()
    8. # Preprocess the data
    9. train_images = train_images.reshape((60000, 28, 28, 1)).astype('float32') / 255
    10. test_images = test_images.reshape((10000, 28, 28, 1)).astype('float32') / 255
    11. train_labels = to_categorical(train_labels)
    12. test_labels = to_categorical(test_labels)
    13. # Define the teacher model (a larger model)
    14. teacher_model = models.Sequential([
    15. layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)),
    16. layers.MaxPooling2D((2, 2)),
    17. layers.Conv2D(64, (3, 3), activation='relu'),
    18. layers.MaxPooling2D((2, 2)),
    19. layers.Conv2D(64, (3, 3), activation='relu'),
    20. layers.Flatten(),
    21. layers.Dense(64, activation='relu'),
    22. layers.Dense(10, activation='softmax')
    23. ])
    24. teacher_model.compile(optimizer='adam',
    25. loss='categorical_crossentropy',
    26. metrics=['accuracy'])
    27. # Train the teacher model
    28. teacher_model.fit(train_images, train_labels, epochs=5, batch_size=64, validation_split=0.2)
    29. # Define the student model (a smaller model)
    30. student_model = models.Sequential([
    31. layers.Flatten(input_shape=(28, 28, 1)),
    32. layers.Dense(64, activation='relu'),
    33. layers.Dense(10, activation='softmax')
    34. ])
    35. student_model.compile(optimizer='adam',
    36. loss='categorical_crossentropy',
    37. metrics=['accuracy'])
    38. # Knowledge distillation step: Transfer knowledge from the teacher to the student
    39. def distillation_loss(y_true, y_pred):
    40. alpha = 0.1 # Temperature parameter (adjust as needed)
    41. return tf.keras.losses.KLDivergence()(tf.nn.softmax(y_true / alpha, axis=1),
    42. tf.nn.softmax(y_pred / alpha, axis=1))
    43. # Train the student model using knowledge distillation
    44. student_model.fit(train_images, train_labels, epochs=10, batch_size=64,
    45. validation_split=0.2, loss=distillation_loss)
    46. # Evaluate the student model
    47. test_loss, test_acc = student_model.evaluate(test_images, test_labels)
    48. print(f'Test accuracy: {test_acc * 100:.2f}%')

    剪枝

    剪枝是我在研究生论文中研究过的一种模型压缩方法,事实上,我之前曾发表过一篇关于如何在Julia中实现剪枝的文章:Julia中用于人工神经网络的迭代剪枝方法。

    剪枝是为了解决决策树中的过拟合问题而诞生的,实际上是通过剪掉树的分支来减小树的深度。该概念后来被用于神经网络,其中会删除网络中的边和/或节点(取决于是否执行非结构化剪枝或结构化剪枝)。

    6b898fea12d0500c3467a2966e7e2296.png

    假设要从网络中删除整个节点,表示层的矩阵将变小,因此您的模型也会变小,因此也会变快。相反,如果我们删除单个边,矩阵的大小将保持不变,但是我们将在删除的边的位置放置零,因此我们将获得非常稀疏的矩阵。因此,在非结构化剪枝中,优势不在于增加速度,而在于内存,因为将稀疏矩阵保存在内存中比保存密集矩阵要占用更少的空间。

    但我们要剪枝的是哪些节点或边呢?通常是最不必要的节点或边,推荐大家可以研究下下面两篇论文:《Optimal Brain Damage》和《Optimal Brain Surgeon and general network pruning》。

    让我们看一个如何在简单的MNIST模型中实现剪枝的Python脚本。

    1. import tensorflow as tf
    2. from tensorflow.keras.models import Sequential
    3. from tensorflow.keras.layers import Dense
    4. from tensorflow.keras.datasets import mnist
    5. from tensorflow.keras.utils import to_categorical
    6. from tensorflow_model_optimization.sparsity import keras as sparsity
    7. import numpy as np
    8. # Load the MNIST dataset
    9. (train_images, train_labels), (test_images, test_labels) = mnist.load_data()
    10. # Preprocess the data
    11. train_images = train_images.reshape((60000, 28, 28, 1)).astype('float32') / 255
    12. test_images = test_images.reshape((10000, 28, 28, 1)).astype('float32') / 255
    13. train_labels = to_categorical(train_labels)
    14. test_labels = to_categorical(test_labels)
    15. # Create a simple neural network model
    16. def create_model():
    17. model = Sequential([
    18. tf.keras.layers.Flatten(input_shape=(28, 28, 1)),
    19. tf.keras.layers.Dense(128, activation='relu'),
    20. tf.keras.layers.Dropout(0.2),
    21. tf.keras.layers.Dense(64, activation='relu'),
    22. tf.keras.layers.Dropout(0.2),
    23. tf.keras.layers.Dense(10, activation='softmax')
    24. ])
    25. return model
    26. # Create and compile the original model
    27. model = create_model()
    28. model.compile(optimizer='adam',
    29. loss='categorical_crossentropy',
    30. metrics=['accuracy'])
    31. # Train the original model
    32. model.fit(train_images, train_labels, epochs=5, batch_size=64, validation_split=0.2)
    33. # Prune the model
    34. # Specify the pruning parameters
    35. pruning_params = {
    36. 'pruning_schedule': sparsity.PolynomialDecay(initial_sparsity=0.50,
    37. final_sparsity=0.90,
    38. begin_step=0,
    39. end_step=2000,
    40. frequency=100)
    41. }
    42. # Create a pruned model
    43. pruned_model = sparsity.prune_low_magnitude(create_model(), **pruning_params)
    44. # Compile the pruned model
    45. pruned_model.compile(optimizer='adam',
    46. loss='categorical_crossentropy',
    47. metrics=['accuracy'])
    48. # Train the pruned model (fine-tuning)
    49. pruned_model.fit(train_images, train_labels, epochs=2, batch_size=64, validation_split=0.2)
    50. # Strip pruning wrappers to create a smaller and faster model
    51. final_model = sparsity.strip_pruning(pruned_model)
    52. # Evaluate the final pruned model
    53. test_loss, test_acc = final_model.evaluate(test_images, test_labels)
    54. print(f'Test accuracy after pruning: {test_acc * 100:.2f}%')

    量化

    我认为没有错的说量化可能是目前最广泛使用的压缩技术。同样,基本思想很简单。通常,我们使用32位浮点数表示神经网络的参数。但如果我们使用更低精度的数值呢?我们可以使用16位、8位、4位,甚至1位,并且拥有二进制网络!

    这意味着什么?通过使用较低精度的数字,模型将更轻,更小,但也会失去精度,提供比原始模型更近似的结果。当我们需要在边缘设备上部署时,特别是在某些特殊硬件上,如智能手机上,这是一种经常使用的技术,因为它允许我们大大缩小网络的大小。许多框架允许轻松应用量化,例如TensorFlow Lite、PyTorch或TensorRT。

    量化可以在训练前应用,因此我们直接截断了一个网络,其参数只能在某个范围内取值,或者在训练后应用,因此最终会对参数的值进行四舍五入。在这里,我们再次快速看一下如何在Python中应用量化。

    1. import tensorflow as tf
    2. from tensorflow.keras.models import Sequential
    3. from tensorflow.keras.layers import Dense, Flatten, Dropout
    4. from tensorflow.keras.datasets import mnist
    5. from tensorflow.keras.utils import to_categorical
    6. import numpy as np
    7. # Load the MNIST dataset
    8. (train_images, train_labels), (test_images, test_labels) = mnist.load_data()
    9. # Preprocess the data
    10. train_images = train_images.reshape((60000, 28, 28, 1)).astype('float32') / 255
    11. test_images = test_images.reshape((10000, 28, 28, 1)).astype('float32') / 255
    12. train_labels = to_categorical(train_labels)
    13. test_labels = to_categorical(test_labels)
    14. # Create a simple neural network model
    15. def create_model():
    16. model = Sequential([
    17. Flatten(input_shape=(28, 28, 1)),
    18. Dense(128, activation='relu'),
    19. Dropout(0.2),
    20. Dense(64, activation='relu'),
    21. Dropout(0.2),
    22. Dense(10, activation='softmax')
    23. ])
    24. return model
    25. # Create and compile the original model
    26. model = create_model()
    27. model.compile(optimizer='adam',
    28. loss='categorical_crossentropy',
    29. metrics=['accuracy'])
    30. # Train the original model
    31. model.fit(train_images, train_labels, epochs=5, batch_size=64, validation_split=0.2)
    32. # Quantize the model to 8-bit integers
    33. converter = tf.lite.TFLiteConverter.from_keras_model(model)
    34. converter.optimizations = [tf.lite.Optimize.DEFAULT]
    35. quantized_model = converter.convert()
    36. # Save the quantized model to a file
    37. with open('quantized_model.tflite', 'wb') as f:
    38. f.write(quantized_model)
    39. # Load the quantized model for inference
    40. interpreter = tf.lite.Interpreter(model_path='quantized_model.tflite')
    41. interpreter.allocate_tensors()
    42. # Evaluate the quantized model
    43. test_loss, test_acc = 0.0, 0.0
    44. for i in range(len(test_images)):
    45. input_data = np.array([test_images[i]], dtype=np.float32)
    46. interpreter.set_tensor(interpreter.get_input_details()[0]['index'], input_data)
    47. interpreter.invoke()
    48. output_data = interpreter.get_tensor(interpreter.get_output_details()[0]['index'])
    49. test_loss += tf.keras.losses.categorical_crossentropy(test_labels[i], output_data).numpy()
    50. test_acc += np.argmax(test_labels[i]) == np.argmax(output_data)
    51. test_loss /= len(test_images)
    52. test_acc /= len(test_images)
    53. print(f'Test accuracy after quantization: {test_acc * 100:.2f}%')

    结论

    在本文中,我们探讨了几种模型压缩方法,以加速模型推断阶段,这对于生产中的模型来说可能是一个关键要求。特别是,我们关注了低秩分解、知识蒸馏、剪枝和量化等方法,解释了基本思想,并展示了Python中的简单实现。模型压缩对于在具有有限资源(RAM、GPU等)的特定硬件上部署模型也非常有用,比如智能手机。

    ·  END  ·

    HAPPY LIFE

    2b8aca1340daff40791d1e7794309c9f.png

    本文仅供学习交流使用,如有侵权请联系作者删除

  • 相关阅读:
    VTK8.0.0编译+QT5.9.2+VS2017
    Linux网络编程基础
    Tensorflow-GPU工具包了解和详细安装方法
    plink如何更新表型数据
    游戏录屏软件推荐,教你录制高清游戏视频
    什么是 PKI? 公钥基础设施的定义和指南
    基于Java+Spring+Vue+elementUI大学生求职招聘系统详细设计实现
    Android - Monkey 测试应用出现Crash报错IllegalStateException
    基于nodejs的电影交流网站
    将可调用对象转换为 c 风格指针
  • 原文地址:https://blog.csdn.net/weixin_38739735/article/details/132843984