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

pat*_*rit 7 java scala jar sbt sbt-assembly

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

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
}
Run Code Online (Sandbox Code Playgroud)

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

最简单的方法是什么?对此有哪些既定范式?我知道它是ML项目中一个相当常见的成语,用于发布经过训练的模型,例如http://nlp.stanford.edu/software/stanford-corenlp-models-current.jar

如何sbt assembly在不必将大型模型类或数据文件签入我的版本控制的情况下执行此操作?

pat*_*rit 0

好吧,我设法做到了这一点:

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

  2. 包含food-trainer大部分代码,并且有一个可以序列化的 main 方法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)
      }
    }
    
    Run Code Online (Sandbox Code Playgroud)
  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")
    }
    
    Run Code Online (Sandbox Code Playgroud)
  4. 现在在你的food-model模块中,你有这样的东西:

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

现在每个下游项目都只依赖food-model并简单地使用FoodModel.model. 我们得到的好处是:

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