SBT:如何将 class 的实例打包为 JAR?

SBT: How to package an instance of a class as a JAR?

我的代码基本上是这样的:

class FoodTrainer(images: S3Path) { // data is >100GB file living in S3
  def train(): FoodClassifier       // Very expensive - takes ~5 hours!
}

class FoodClassifier {          // Light-weight API class
  def isHotDog(input: Image): Boolean
}

我想在 JAR 组装 (sbt assembly) 时,调用 val classifier = new FoodTrainer(s3Dir).train() 并发布 JAR,其中 classifier 实例立即可供下游库用户使用。

最简单的方法是什么?这方面有哪些既定范例?我知道在 ML 项目中发布训练有素的模型是一个相当常见的习惯用法,例如http://nlp.stanford.edu/software/stanford-corenlp-models-current.jar

如何使用 sbt assembly 执行此操作,而不必将大型模型 class 或数据文件签入我的版本控制?

这是一个想法,将您的模型放入一个资源文件夹中,然后将其添加到 jar 程序集中。我认为如果模型在该文件夹中,所有 jar 都会与您的模型一起分发。看看进展如何,干杯!

查看此资源以阅读资源:

https://www.mkyong.com/java/java-read-a-file-from-resources-folder/

它在 Java 中,但您仍然可以在 Scala 中使用 api。

您应该将训练产生的数据序列化到它自己的文件中。然后,您可以将此数据文件打包到您的 JAR 中。您的生产代码打开文件并读取它而不是 运行 训练算法。

步骤如下

在构建的资源生成阶段:

  1. 在构建的资源生成阶段生成模型。
  2. 将模型的内容序列化为托管资源文件夹中的文件。
    resourceGenerators in Compile += Def.task {
      val classifier = new FoodTrainer(s3Dir).train()
      val contents = FoodClassifier.serialize(classifier)
      val file = (resourceManaged in Compile).value / "mypackage" / "food-classifier.model"
      IO.write(file, contents)
      Seq(file)
    }.taskValue
    
  3. 资源将自动包含在 jar 文件中,不会出现在源代码树中。
  4. 要加载模型,只需添加读取资源和解析模型的代码。
    object FoodClassifierModel {
      lazy val classifier = readResource("/mypackage/food-classifier.model")
      def readResource(resourceName: String): FoodClassifier = {
        val stream = getClass.getResourceAsStream(resourceName)
        val lines = scala.io.Source.fromInputStream( stream ).getLines
        val contents = lines.mkString("\n")
        FoodClassifier.parse(contents)
      }
    }
    object FoodClassifier {
      def parse(content: String): FoodClassifier
      def serialize(classfier: FoodClassifier): String
    }
    

当然,由于您的数据相当大,您需要使用流序列化器和解析器来避免 java 堆 space 过载。以上只是构建时如何打包资源

http://www.scala-sbt.org/1.x/docs/Howto-Generating-Files.html

好的,我做到了:

  1. 将食物训练器模块分成 2 个独立的 SBT 子模块:food-trainerfood-model。前者 仅在编译时调用以创建模型并序列化到后者的生成资源 中。后者用作一个简单的工厂对象,用于从序列化版本实例化模型。每个下游项目只依赖这个 food-model 子模块。

  2. food-trainer 包含大部分代码,并且有一个主要方法可以序列化 FoodModel:

    object FoodTrainer {
      def main(args Array[String]): Unit = {
        val input = args(0)
        val outputDir = args(1)
        val model: FoodModel = new FoodTrainer(input).train() 
        val out = new ObjectOutputStream(new File(outputDir + "/model.bin"))
        out.writeObject(model)
      }
    }
    
  3. 在你的 build.sbt:

    添加一个编译时任务来生成食物训练器模块
    lazy val foodTrainer = (project in file("food-trainer"))
    
    lazy val foodModel = (project in file("food-model"))
      .dependsOn(foodTrainer)
      .settings(    
         resourceGenerators in Compile += Def.task {
           val log = streams.value.log
           val dest = (resourceManaged in Compile).value   
           IO.createDirectory(dest)
           runModuleMain(
             cmd = s"com.foo.bar.FoodTrainer $pathToImages ${dest.getAbsolutePath}",
             cp = (fullClasspath in Runtime in foodTrainer).value.files,
             log = log
           )             
          Seq(dest / "model.bin")
        }
    
    def runModuleMain(cmd: String, cp: Seq[File], log: Logger): Unit = {
      log.info(s"Running $cmd")
      val opt = ForkOptions(bootJars = cp, outputStrategy = Some(LoggedOutput(log)))
      val res = Fork.scala(config = opt, arguments = cmd.split(' '))
      require(res == 0, s"$cmd exited with code $res")
    }
    
  4. 现在在你的 food-model 模块中,你有这样的东西:

    object FoodModel {
      lazy val model: FoodModel =
        new ObjectInputStream(getClass.getResourceAsStream("/model.bin").readObject().asInstanceOf[FoodModel])
    }
    

每个下游项目现在只依赖 food-model 并且只使用 FoodModel.model。我们受益于:

  1. 这是在运行时从 JAR 中快速静态加载的 打包资源
  2. 无需在运行时训练模型(非常 贵)
  3. 无需在您的版本中签入模型 控制(同样二进制模型非常大) - 它只被打包到你的 罐子
  4. 不需要分开FoodTrainerFoodModel 将它们打包到它们自己的 JAR 中(我们现在很头疼在内部部署它们)——相反,我们只是将它们保持在相同的位置 项目但不同的子模块被打包到一个 JAR 中。