├── .gitignore ├── .gitmodules ├── .travis.yml ├── LICENSE ├── README.md ├── build.sbt ├── core └── src │ └── main │ ├── resources │ └── pc │ └── scala │ └── tensorflow │ ├── TensorFlowExample.scala │ ├── TensorFlowProvider.scala │ └── model │ ├── InceptionV3.scala │ ├── Labelable.scala │ └── TensorFlowModel.scala ├── cropped_panda.jpg ├── model └── download.sh ├── project ├── build.properties └── plugins.sbt ├── rest └── src │ └── main │ └── scala │ └── org │ └── mskim │ └── tensorflow │ ├── Server.scala │ └── controllers │ └── TensorFlowServingController.scala └── sbt /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | target 3 | model/LICENSE 4 | model/cropped_panda.jpg 5 | model/imagenet_2012_challenge_label_map_proto.pbtxt 6 | model/classify_image_graph_def.pb 7 | model/imagenet_synset_to_human_label_map.txt 8 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mskimorg/tensorflow-scala/07458da45dd118cd9e84d53d8cb250c178ddc020/.gitmodules -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | scala: 3 | - 2.11.8 4 | 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/mskimm/tensorflow-scala.svg?branch=master)](https://travis-ci.org/mskimm/tensorflow-scala) 2 | 3 | # A Scala binding of TensorFlow for Serving TensorFlow Models 4 | 5 | ## Quick Start 6 | ``` 7 | $ git clone https://github.com/mskimm/tensorflow-scala.git 8 | $ cd tensorflow-scala 9 | $ (cd model; sh download.sh) # download inception-v3 10 | # start rest server 11 | $ sbt rest/run 12 | ``` 13 | then post `cropped_panda.jpg` in another terminal using `curl` 14 | ``` 15 | $ curl -XPOST -F "image=@cropped_panda.jpg" localhost:8888/v1/image/label 16 | ``` 17 | shows 18 | ``` 19 | [ 20 | { 21 | "code": "n02510455", 22 | "label": "giant panda, panda, panda bear, coon bear, Ailuropoda melanoleuca", 23 | "score": 0.8910737 24 | }, 25 | { 26 | "code": "n02500267", 27 | "label": "indri, indris, Indri indri, Indri brevicaudatus", 28 | "score": 0.007790538 29 | }, 30 | { 31 | "code": "n02509815", 32 | "label": "lesser panda, red panda, panda, bear cat, cat bear, Ailurus fulgens", 33 | "score": 0.0029591226 34 | }, 35 | { 36 | "code": "n07760859", 37 | "label": "custard apple", 38 | "score": 0.0014657712 39 | }, 40 | { 41 | "code": "n13044778", 42 | "label": "earthstar", 43 | "score": 0.0011742385 44 | } 45 | ] 46 | ``` 47 | 48 | where `cropped_panda.jpg` is: 49 | 50 | ![cropped_panda](https://raw.githubusercontent.com/mskimm/tensorflow-scala/master/cropped_panda.jpg) 51 | 52 | # History 53 | - 16 June 2017 - Changed the objectives of this project to `Serving TensorFlow Models` with Scala 54 | - 11 May 2017 - Decided to deprecate this project because TensorFlow provides Java API from the version 1.0.0. 55 | - 19 Oct 2016 - This project started with JNA binding of TensorFlow. 56 | 57 | 58 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | lazy val commonSettings = Seq( 2 | organization := "org.mskim", 3 | scalaVersion := "2.11.8", 4 | version := "0.0.3-SNAPSHOT", 5 | licenses += "Apache-2.0" -> url("https://www.apache.org/licenses/LICENSE-2.0.html") 6 | ) 7 | 8 | lazy val core = project 9 | .settings(commonSettings) 10 | .settings(Seq( 11 | libraryDependencies ++= Seq( 12 | "org.tensorflow" % "tensorflow" % "1.2.1", 13 | "com.github.fommil.netlib" % "all" % "1.1.2" pomOnly() 14 | ) 15 | )) 16 | 17 | lazy val rest = project 18 | .dependsOn(core) 19 | .settings(commonSettings) 20 | .settings(Seq( 21 | resolvers += "Twitter Maven" at "http://maven.twttr.com", 22 | libraryDependencies ++= Seq( 23 | "com.twitter" %% "finatra-http" % "2.11.0", 24 | "ch.qos.logback" % "logback-classic" % "1.1.7" 25 | ), 26 | excludeDependencies += "org.slf4j" % "slf4j-log4j12" 27 | )) 28 | 29 | lazy val root = (project in file(".")) 30 | .aggregate(core, rest) 31 | .dependsOn(core, rest) 32 | .settings(commonSettings) 33 | 34 | -------------------------------------------------------------------------------- /core/src/main/resources/pc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mskimorg/tensorflow-scala/07458da45dd118cd9e84d53d8cb250c178ddc020/core/src/main/resources/pc -------------------------------------------------------------------------------- /core/src/main/scala/tensorflow/TensorFlowExample.scala: -------------------------------------------------------------------------------- 1 | package tensorflow 2 | 3 | import java.io.{BufferedInputStream, ByteArrayOutputStream} 4 | import java.net.URL 5 | import java.nio.file.{Files, Paths} 6 | 7 | import tensorflow.model.InceptionV3 8 | 9 | object TensorFlowExample { 10 | 11 | def main(args: Array[String]) { 12 | // image file 13 | val jpgFile = args.headOption.getOrElse("cropped_panda.jpg") 14 | val jpgAsBytes = jpgFile match { 15 | case urlString if urlString.startsWith("http") => 16 | val url = new URL(urlString) 17 | val in = new BufferedInputStream(url.openStream()) 18 | val out = new ByteArrayOutputStream() 19 | val buf = new Array[Byte](1024) 20 | var n = in.read(buf) 21 | while (n != -1) { 22 | out.write(buf, 0, n) 23 | n = in.read(buf) 24 | } 25 | val bytes = out.toByteArray 26 | out.close() 27 | in.close() 28 | bytes 29 | case file => Files.readAllBytes(Paths.get(file)) 30 | } 31 | 32 | // define the model 33 | val model = new InceptionV3("model") 34 | 35 | // initialize TensorFlowProvider 36 | val provider = new TensorFlowProvider(model) 37 | 38 | // setting up input and output layers to classify 39 | val inputLayer = "DecodeJpeg/contents" 40 | val outputLayer = "softmax" 41 | 42 | // get result of the outputLayer 43 | val result = provider.run(inputLayer -> jpgAsBytes, outputLayer) 44 | 45 | // get label of the top 5 46 | val label = model.getLabelOf(result.head, 5) 47 | 48 | // print out 49 | label foreach println 50 | 51 | // shows ... 52 | // 53 | // Label(n02510455,giant panda, panda, panda bear, coon bear, Ailuropoda melanoleuca,0.8910737) 54 | // Label(n02500267,indri, indris, Indri indri, Indri brevicaudatus,0.007790538) 55 | // Label(n02509815,lesser panda, red panda, panda, bear cat, cat bear, Ailurus fulgens,0.0029591226) 56 | // Label(n07760859,custard apple,0.0014657712) 57 | // Label(n13044778,earthstar,0.0011742385) 58 | 59 | // release resources 60 | provider.close() 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /core/src/main/scala/tensorflow/TensorFlowProvider.scala: -------------------------------------------------------------------------------- 1 | package tensorflow 2 | 3 | import java.io.ByteArrayOutputStream 4 | import java.nio.ByteBuffer 5 | import java.nio.file.Files 6 | 7 | import com.github.fommil.netlib.BLAS 8 | import org.tensorflow.{Graph, Session, Tensor} 9 | import tensorflow.model.TensorFlowModel 10 | 11 | import scala.collection.JavaConverters._ 12 | 13 | class TensorFlowProvider(model: TensorFlowModel) extends AutoCloseable { 14 | 15 | private val blas = BLAS.getInstance() 16 | 17 | private val (pc, rows, cols) = { 18 | val pcBytes: Array[Byte] = { 19 | val input = getClass.getResourceAsStream("/pc") 20 | val output = new ByteArrayOutputStream() 21 | val buffer = new Array[Byte](4096) 22 | var n = input.read(buffer) 23 | while (-1 != n) { 24 | output.write(buffer, 0, n) 25 | n = input.read(buffer) 26 | } 27 | output.toByteArray 28 | } 29 | val bf = ByteBuffer.wrap(pcBytes) 30 | val rows = bf.getInt() 31 | val cols = bf.getInt() 32 | val floats = new Array[Float](bf.remaining() / 4) 33 | floats.indices foreach { i => floats(i) = bf.getFloat } 34 | (floats, rows, cols) 35 | } 36 | 37 | private val graph: Graph = { 38 | val graph = new Graph() 39 | graph.importGraphDef(model.getBytes) 40 | graph 41 | } 42 | 43 | private val session: Session = new Session(graph) 44 | 45 | def run(i1: (String, Any), outputs: String*): Seq[Array[Float]] = 46 | run(Seq(i1), outputs: _*) 47 | 48 | def run(i1: (String, Any), i2: (String, Any), outputs: String*): Seq[Array[Float]] = 49 | run(Seq(i1, i2), outputs: _*) 50 | 51 | def run(i1: (String, Any), i2: (String, Any), i3: (String, Any), outputs: String*): Seq[Array[Float]] = 52 | run(Seq(i1, i2, i3), outputs: _*) 53 | 54 | def run(i1: (String, Any), i2: (String, Any), i3: (String, Any), i4: (String, Any), outputs: String*): Seq[Array[Float]] = 55 | run(Seq(i1, i2, i3, i4), outputs: _*) 56 | 57 | def run(i1: (String, Any), i2: (String, Any), i3: (String, Any), i4: (String, Any), i5: (String, Any), outputs: String*): Seq[Array[Float]] = 58 | run(Seq(i1, i2, i3, i4, i5), outputs: _*) 59 | 60 | def run(input: Seq[(String, Any)], output: String*): Seq[Array[Float]] = { 61 | val runner = session.runner() 62 | 63 | val inputTensors: Seq[(String, Tensor)] = input.map { case (op, obj) => 64 | op -> Tensor.create(obj) 65 | } 66 | 67 | // feed 68 | inputTensors foreach { case (op, tensor) => 69 | runner.feed(op, tensor) 70 | } 71 | 72 | // fetch 73 | output foreach runner.fetch 74 | 75 | // run 76 | val resultTensors: Seq[Tensor] = runner.run().asScala 77 | 78 | val result: Seq[Array[Float]] = resultTensors.map { tensor => 79 | val size = tensor.numElements() 80 | val to = Array(new Array[Float](size)) 81 | tensor.copyTo(to) 82 | to(0) 83 | } 84 | 85 | // release 86 | inputTensors.foreach(_._2.close()) 87 | resultTensors.foreach(_.close()) 88 | 89 | result 90 | } 91 | 92 | def reduce(features: Array[Float]): Array[Float] = { 93 | val projected = new Array[Float](cols) 94 | blas.sgemv("T", rows, cols, 1f, pc, rows, features, 1, 0, projected, 1) 95 | projected 96 | } 97 | 98 | override def close(): Unit = { 99 | session.close() 100 | graph.close() 101 | } 102 | 103 | } 104 | 105 | -------------------------------------------------------------------------------- /core/src/main/scala/tensorflow/model/InceptionV3.scala: -------------------------------------------------------------------------------- 1 | package tensorflow.model 2 | 3 | import java.nio.charset.Charset 4 | import java.nio.file.{Files, Paths} 5 | 6 | import scala.collection.JavaConverters._ 7 | 8 | class InceptionV3(graphPath: String, humanLabelPath: String, labelMapPath: String) extends TensorFlowModel with Labelable { 9 | 10 | def this(modelPath: String) = this( 11 | s"$modelPath/classify_image_graph_def.pb", 12 | s"$modelPath/imagenet_synset_to_human_label_map.txt", 13 | s"$modelPath/imagenet_2012_challenge_label_map_proto.pbtxt") 14 | 15 | private val codeLabelSeq: Array[(String, String)] = { 16 | val labelMap = Files.readAllLines(Paths.get(humanLabelPath), Charset.defaultCharset()).asScala 17 | .map(_.split("\\s+", 2)) 18 | .map { case Array(s, l) => (s.trim, l.trim) } 19 | .toMap 20 | 21 | val indexToCode = Files.readAllLines(Paths.get(labelMapPath), Charset.defaultCharset()).asScala 22 | .dropWhile(_.trim.startsWith("#")) 23 | .grouped(4) 24 | .map { grouped => 25 | val targetClass = grouped(1).split(":")(1).trim.toInt 26 | val targetClassString = grouped(2).split(":")(1).trim.stripPrefix("\"").stripSuffix("\"") 27 | (targetClass, (targetClassString, labelMap(targetClassString))) 28 | } 29 | .toMap 30 | .withDefault(_ => "" -> "") 31 | 32 | Array.tabulate(indexToCode.keys.max + 1)(indexToCode.apply) 33 | } 34 | 35 | override def getBytes: Array[Byte] = 36 | Files.readAllBytes(Paths.get(graphPath)) 37 | 38 | override def getLabelOf(tensor: Array[Float], limit: Int): Seq[Label] = { 39 | val all = tensor.zip(codeLabelSeq).map { case (score, (code, label)) => 40 | Label(code, label, score) 41 | } 42 | all.sortBy(-_.score).take(limit).toSeq 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /core/src/main/scala/tensorflow/model/Labelable.scala: -------------------------------------------------------------------------------- 1 | package tensorflow.model 2 | 3 | case class Label( 4 | code: String, 5 | label: String, 6 | score: Float 7 | ) 8 | 9 | trait Labelable { 10 | 11 | def getLabelOf(tensor: Array[Float], limit: Int = 10): Seq[Label] 12 | 13 | } 14 | -------------------------------------------------------------------------------- /core/src/main/scala/tensorflow/model/TensorFlowModel.scala: -------------------------------------------------------------------------------- 1 | package tensorflow.model 2 | 3 | trait TensorFlowModel { 4 | 5 | def getBytes: Array[Byte] 6 | 7 | } 8 | -------------------------------------------------------------------------------- /cropped_panda.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mskimorg/tensorflow-scala/07458da45dd118cd9e84d53d8cb250c178ddc020/cropped_panda.jpg -------------------------------------------------------------------------------- /model/download.sh: -------------------------------------------------------------------------------- 1 | curl http://download.tensorflow.org/models/image/imagenet/inception-2015-12-05.tgz | tar xvz 2 | 3 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.8 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | logLevel := Level.Warn 2 | 3 | addSbtPlugin("me.lessis" % "bintray-sbt" % "0.3.0") 4 | -------------------------------------------------------------------------------- /rest/src/main/scala/org/mskim/tensorflow/Server.scala: -------------------------------------------------------------------------------- 1 | package org.mskim.tensorflow 2 | 3 | import com.twitter.finatra.http.HttpServer 4 | import com.twitter.finatra.http.filters.CommonFilters 5 | import com.twitter.finatra.http.routing.HttpRouter 6 | import org.mskim.tensorflow.controllers.TensorFlowServingController 7 | 8 | object ServerMain extends Server 9 | 10 | class Server extends HttpServer { 11 | 12 | override def configureHttp(router: HttpRouter) { 13 | router 14 | .filter[CommonFilters] 15 | .add[TensorFlowServingController] 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /rest/src/main/scala/org/mskim/tensorflow/controllers/TensorFlowServingController.scala: -------------------------------------------------------------------------------- 1 | package org.mskim.tensorflow.controllers 2 | 3 | import java.nio.ByteBuffer 4 | 5 | import com.google.common.io.BaseEncoding 6 | import com.twitter.finagle.http.Request 7 | import com.twitter.finatra.http.Controller 8 | import com.twitter.finatra.http.request.RequestUtils 9 | import com.twitter.util.{Future, FuturePool} 10 | import tensorflow.TensorFlowProvider 11 | import tensorflow.model.InceptionV3 12 | 13 | class TensorFlowServingController extends Controller { 14 | 15 | // define the model 16 | val model = new InceptionV3("model") 17 | 18 | // initialize TensorFlowProvider 19 | val provider = new TensorFlowProvider(model) 20 | 21 | // setting up input and output layers to classify 22 | val inputLayer = "DecodeJpeg/contents" 23 | val outputLayer = "softmax" 24 | val bottleneckLayer = "pool_3/_reshape" 25 | 26 | post("/v1/image/label") { request: Request => 27 | val multiParams = RequestUtils.multiParams(request) 28 | multiParams.get("image") match { 29 | case Some(image) => 30 | FuturePool.interruptibleUnboundedPool { 31 | val result = provider.run(inputLayer -> image.data, outputLayer) 32 | model.getLabelOf(result.head, 5) 33 | } 34 | case None => Future.None 35 | } 36 | } 37 | post("/v1/image/features") { request: Request => 38 | val reducing = request.getBooleanParam("reduce") 39 | val multiParams = RequestUtils.multiParams(request) 40 | multiParams.get("image") match { 41 | case Some(image) => 42 | FuturePool.interruptibleUnboundedPool { 43 | val features0 = provider.run(inputLayer -> image.data, bottleneckLayer).head 44 | val features = if (reducing) { 45 | provider.reduce(features0) 46 | } else { 47 | features0 48 | } 49 | val bf = ByteBuffer.allocate(features.length * 4) 50 | features foreach bf.putFloat 51 | BaseEncoding.base64().encode(bf.array()) 52 | } 53 | case None => Future.None 54 | } 55 | } 56 | 57 | 58 | } 59 | -------------------------------------------------------------------------------- /sbt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # A more capable sbt runner, coincidentally also called sbt. 4 | # Author: Paul Phillips 5 | 6 | # todo - make this dynamic 7 | declare -r sbt_release_version=0.12.0 8 | declare -r sbt_snapshot_version=0.13.0-SNAPSHOT 9 | 10 | unset sbt_jar sbt_dir sbt_create sbt_snapshot sbt_launch_dir 11 | unset scala_version java_home sbt_explicit_version 12 | unset verbose debug quiet 13 | 14 | for arg in "$@"; do 15 | case $arg in 16 | -q|-quiet) quiet=1 ;; 17 | *) ;; 18 | esac 19 | done 20 | 21 | build_props_sbt () { 22 | if [[ -f project/build.properties ]]; then 23 | versionLine=$(grep ^sbt.version project/build.properties) 24 | versionString=${versionLine##sbt.version=} 25 | echo "$versionString" 26 | fi 27 | } 28 | 29 | update_build_props_sbt () { 30 | local ver="$1" 31 | local old=$(build_props_sbt) 32 | 33 | if [[ $ver == $old ]]; then 34 | return 35 | elif [[ -f project/build.properties ]]; then 36 | perl -pi -e "s/^sbt\.version=.*\$/sbt.version=${ver}/" project/build.properties 37 | grep -q '^sbt.version=' project/build.properties || echo "sbt.version=${ver}" >> project/build.properties 38 | 39 | echo !!! 40 | echo !!! Updated file project/build.properties setting sbt.version to: $ver 41 | echo !!! Previous value was: $old 42 | echo !!! 43 | fi 44 | } 45 | 46 | sbt_version () { 47 | if [[ -n $sbt_explicit_version ]]; then 48 | echo $sbt_explicit_version 49 | else 50 | local v=$(build_props_sbt) 51 | if [[ -n $v ]]; then 52 | echo $v 53 | else 54 | echo $sbt_release_version 55 | fi 56 | fi 57 | } 58 | 59 | echoerr () { 60 | [[ -z $quiet ]] && echo 1>&2 "$@" 61 | } 62 | vlog () { 63 | [[ $verbose || $debug ]] && echoerr "$@" 64 | } 65 | dlog () { 66 | [[ $debug ]] && echoerr "$@" 67 | } 68 | 69 | # this seems to cover the bases on OSX, and someone will 70 | # have to tell me about the others. 71 | get_script_path () { 72 | local path="$1" 73 | [[ -L "$path" ]] || { echo "$path" ; return; } 74 | 75 | local target=$(readlink "$path") 76 | if [[ "${target:0:1}" == "/" ]]; then 77 | echo "$target" 78 | else 79 | echo "$(dirname $path)/$target" 80 | fi 81 | } 82 | 83 | # a ham-fisted attempt to move some memory settings in concert 84 | # so they need not be dicked around with individually. 85 | get_mem_opts () { 86 | local mem=${1:-1536} 87 | local perm=$(( $mem / 4 )) 88 | (( $perm > 256 )) || perm=256 89 | (( $perm < 1024 )) || perm=1024 90 | local codecache=$(( $perm / 2 )) 91 | 92 | echo "-Xms${mem}m -Xmx${mem}m -XX:MaxPermSize=${perm}m -XX:ReservedCodeCacheSize=${codecache}m" 93 | } 94 | 95 | die() { 96 | echo "Aborting: $@" 97 | exit 1 98 | } 99 | 100 | make_url () { 101 | groupid="$1" 102 | category="$2" 103 | version="$3" 104 | 105 | echo "http://repo.typesafe.com/typesafe/ivy-$category/$groupid/sbt-launch/$version/sbt-launch.jar" 106 | } 107 | 108 | declare -r default_jvm_opts="-Dfile.encoding=UTF8" 109 | declare -r default_sbt_opts="-XX:+CMSClassUnloadingEnabled" 110 | declare -r default_sbt_mem=1536 111 | declare -r noshare_opts="-Dsbt.global.base=project/.sbtboot -Dsbt.boot.directory=project/.boot -Dsbt.ivy.home=project/.ivy" 112 | declare -r sbt_opts_file=".sbtopts" 113 | declare -r jvm_opts_file=".jvmopts" 114 | declare -r latest_28="2.8.2" 115 | declare -r latest_29="2.9.2" 116 | declare -r latest_210="2.10.0-SNAPSHOT" 117 | 118 | declare -r script_path=$(get_script_path "$BASH_SOURCE") 119 | declare -r script_dir="$(dirname $script_path)" 120 | declare -r script_name="$(basename $script_path)" 121 | 122 | # some non-read-onlies set with defaults 123 | declare java_cmd=java 124 | declare sbt_launch_dir="$script_dir/.lib" 125 | declare sbt_universal_launcher="$script_dir/lib/sbt-launch.jar" 126 | declare sbt_mem=$default_sbt_mem 127 | declare sbt_jar=$sbt_universal_launcher 128 | 129 | # pull -J and -D options to give to java. 130 | declare -a residual_args 131 | declare -a java_args 132 | declare -a scalac_args 133 | declare -a sbt_commands 134 | 135 | build_props_scala () { 136 | if [[ -f project/build.properties ]]; then 137 | versionLine=$(grep ^build.scala.versions project/build.properties) 138 | versionString=${versionLine##build.scala.versions=} 139 | echo ${versionString%% .*} 140 | fi 141 | } 142 | 143 | execRunner () { 144 | # print the arguments one to a line, quoting any containing spaces 145 | [[ $verbose || $debug ]] && echo "# Executing command line:" && { 146 | for arg; do 147 | if printf "%s\n" "$arg" | grep -q ' '; then 148 | printf "\"%s\"\n" "$arg" 149 | else 150 | printf "%s\n" "$arg" 151 | fi 152 | done 153 | echo "" 154 | } 155 | 156 | exec "$@" 157 | } 158 | 159 | sbt_groupid () { 160 | case $(sbt_version) in 161 | 0.7.*) echo org.scala-tools.sbt ;; 162 | 0.10.*) echo org.scala-tools.sbt ;; 163 | 0.11.[12]) echo org.scala-tools.sbt ;; 164 | *) echo org.scala-sbt ;; 165 | esac 166 | } 167 | 168 | sbt_artifactory_list () { 169 | local version0=$(sbt_version) 170 | local version=${version0%-SNAPSHOT} 171 | local url="http://typesafe.artifactoryonline.com/typesafe/ivy-snapshots/$(sbt_groupid)/sbt-launch/" 172 | dlog "Looking for snapshot list at: $url " 173 | 174 | curl -s --list-only "$url" | \ 175 | grep -F $version | \ 176 | perl -e 'print reverse <>' | \ 177 | perl -pe 's#^/dev/null 191 | dlog "curl returned: $?" 192 | echo "$url" 193 | return 194 | done 195 | } 196 | 197 | jar_url () { 198 | case $(sbt_version) in 199 | 0.7.*) echo "http://simple-build-tool.googlecode.com/files/sbt-launch-0.7.7.jar" ;; 200 | *-SNAPSHOT) make_snapshot_url ;; 201 | *) make_release_url ;; 202 | esac 203 | } 204 | 205 | jar_file () { 206 | echo "$sbt_launch_dir/$1/sbt-launch.jar" 207 | } 208 | 209 | download_url () { 210 | local url="$1" 211 | local jar="$2" 212 | 213 | echo "Downloading sbt launcher $(sbt_version):" 214 | echo " From $url" 215 | echo " To $jar" 216 | 217 | mkdir -p $(dirname "$jar") && { 218 | if which curl >/dev/null; then 219 | curl -L --fail --silent "$url" --output "$jar" 220 | elif which wget >/dev/null; then 221 | wget --quiet -O "$jar" "$url" 222 | fi 223 | } && [[ -f "$jar" ]] 224 | } 225 | 226 | acquire_sbt_jar () { 227 | sbt_url="$(jar_url)" 228 | sbt_jar="$(jar_file $(sbt_version))" 229 | 230 | [[ -f "$sbt_jar" ]] || download_url "$sbt_url" "$sbt_jar" 231 | } 232 | 233 | usage () { 234 | cat < path to global settings/plugins directory (default: ~/.sbt/) 243 | -sbt-boot path to shared boot directory (default: ~/.sbt/boot in 0.11+) 244 | -ivy path to local Ivy repository (default: ~/.ivy2) 245 | -mem set memory options (default: $sbt_mem, which is 246 | $(get_mem_opts $sbt_mem) ) 247 | -no-share use all local caches; no sharing 248 | -offline put sbt in offline mode 249 | -jvm-debug Turn on JVM debugging, open at the given port. 250 | -batch Disable interactive mode 251 | # sbt version (default: from project/build.properties if present, else latest release) 252 | !!! The only way to accomplish this pre-0.12.0 if there is a build.properties file which 253 | !!! contains an sbt.version property is to update the file on disk. That's what this does. 254 | -sbt-version use the specified version of sbt 255 | -sbt-jar use the specified jar as the sbt launcher 256 | -sbt-snapshot use a snapshot version of sbt 257 | -sbt-launch-dir directory to hold sbt launchers (default: $sbt_launch_dir) 258 | # scala version (default: as chosen by sbt) 259 | -28 use $latest_28 260 | -29 use $latest_29 261 | -210 use $latest_210 262 | -scala-home use the scala build at the specified directory 263 | -scala-version use the specified version of scala 264 | # java version (default: java from PATH, currently $(java -version |& grep version)) 265 | -java-home alternate JAVA_HOME 266 | # jvm options and output control 267 | JAVA_OPTS environment variable holding jvm args, if unset uses "$default_jvm_opts" 268 | SBT_OPTS environment variable holding jvm args, if unset uses "$default_sbt_opts" 269 | .jvmopts if file is in sbt root, it is prepended to the args given to the jvm 270 | .sbtopts if file is in sbt root, it is prepended to the args given to **sbt** 271 | -Dkey=val pass -Dkey=val directly to the jvm 272 | -J-X pass option -X directly to the jvm (-J is stripped) 273 | -S-X add -X to sbt's scalacOptions (-J is stripped) 274 | In the case of duplicated or conflicting options, the order above 275 | shows precedence: JAVA_OPTS lowest, command line options highest. 276 | EOM 277 | } 278 | 279 | addJava () { 280 | dlog "[addJava] arg = '$1'" 281 | java_args=( "${java_args[@]}" "$1" ) 282 | } 283 | addSbt () { 284 | dlog "[addSbt] arg = '$1'" 285 | sbt_commands=( "${sbt_commands[@]}" "$1" ) 286 | } 287 | addScalac () { 288 | dlog "[addScalac] arg = '$1'" 289 | scalac_args=( "${scalac_args[@]}" "$1" ) 290 | } 291 | addResidual () { 292 | dlog "[residual] arg = '$1'" 293 | residual_args=( "${residual_args[@]}" "$1" ) 294 | } 295 | addResolver () { 296 | addSbt "set resolvers in ThisBuild += $1" 297 | } 298 | addDebugger () { 299 | addJava "-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=$1" 300 | } 301 | 302 | jrebelAgent () { 303 | SCALATRA_PROJECT_ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 304 | if [ -z "$SCALATRA_JREBEL" ]; 305 | then echo -n ''; 306 | else echo -n "-javaagent:$SCALATRA_JREBEL -Dscalatra_project_root=${SCALATRA_PROJECT_ROOT}"; 307 | fi 308 | } 309 | 310 | get_jvm_opts () { 311 | echo "${JAVA_OPTS:-$default_jvm_opts}" 312 | echo "`jrebelAgent` ${SBT_OPTS:-$default_sbt_opts}" 313 | 314 | [[ -f "$jvm_opts_file" ]] && cat "$jvm_opts_file" 315 | } 316 | 317 | process_args () 318 | { 319 | require_arg () { 320 | local type="$1" 321 | local opt="$2" 322 | local arg="$3" 323 | 324 | if [[ -z "$arg" ]] || [[ "${arg:0:1}" == "-" ]]; then 325 | die "$opt requires <$type> argument" 326 | fi 327 | } 328 | while [[ $# -gt 0 ]]; do 329 | case "$1" in 330 | -h|-help) usage; exit 1 ;; 331 | -v|-verbose) verbose=1 && shift ;; 332 | -d|-debug) debug=1 && shift ;; 333 | -q|-quiet) quiet=1 && shift ;; 334 | 335 | -ivy) require_arg path "$1" "$2" && addJava "-Dsbt.ivy.home=$2" && shift 2 ;; 336 | -mem) require_arg integer "$1" "$2" && sbt_mem="$2" && shift 2 ;; 337 | -no-colors) addJava "-Dsbt.log.noformat=true" && shift ;; 338 | -no-share) addJava "$noshare_opts" && shift ;; 339 | -sbt-boot) require_arg path "$1" "$2" && addJava "-Dsbt.boot.directory=$2" && shift 2 ;; 340 | -sbt-dir) require_arg path "$1" "$2" && sbt_dir="$2" && shift 2 ;; 341 | -debug-inc) addJava "-Dxsbt.inc.debug=true" && shift ;; 342 | -offline) addSbt "set offline := true" && shift ;; 343 | -jvm-debug) require_arg port "$1" "$2" && addDebugger $2 && shift 2 ;; 344 | -batch) exec 0 )) || echo "Starting $script_name: invoke with -help for other options" 406 | 407 | # verify this is an sbt dir or -create was given 408 | [[ -f ./build.sbt || -d ./project || -n "$sbt_create" ]] || { 409 | cat <