官术网_书友最值得收藏!

1.8.4 開(kāi)發(fā)一個(gè)基于移動(dòng)端深度學(xué)習(xí)框架的Android App

在開(kāi)始創(chuàng)建Android App之前,需要下載并安裝Android Studio 3或以上版本。由于新版本的Android Studio已經(jīng)默認(rèn)安裝了Android SDK,所以整個(gè)過(guò)程會(huì)比較方便(如果涉及對(duì)Google的訪問(wèn),可能需要配置代理)。

安裝Android Studio以后,創(chuàng)建一個(gè)新項(xiàng)目,名稱(chēng)自擬即可。由于Kotlin語(yǔ)言簡(jiǎn)單明了,所以為了快速成型,筆者在這選擇了Kotlin。不論是Java還是Kotlin,都不會(huì)影響工程的創(chuàng)建和開(kāi)發(fā),只要選擇你認(rèn)為最容易實(shí)現(xiàn)的語(yǔ)言就可以。

本節(jié)使用的源碼見(jiàn)“鏈接6”。雖然可以復(fù)制源碼并直接運(yùn)行,但仍然建議參照源碼從零開(kāi)始搭建并編寫(xiě)這一部分代碼,這樣可以更深刻地理解一個(gè)簡(jiǎn)單的視覺(jué)神經(jīng)網(wǎng)絡(luò)程序在移動(dòng)端運(yùn)行的步驟。

有了IDE等基本環(huán)境后,建立一個(gè)基礎(chǔ)工程,如圖1-19所示:

圖1-19 在Android平臺(tái)創(chuàng)建基礎(chǔ)工程

在GitHub上的Paddle-Lite項(xiàng)目中可以找到一些測(cè)試模型下載地址,截至2019年8月, GitHub社區(qū)使用的模型見(jiàn)“鏈接7”。這些測(cè)試模型可以用于開(kāi)發(fā)相關(guān)程序。

將模型包下載到本地并解壓,就能得到一系列測(cè)試模型。在本例中,筆者使用的模型是MobileNet,從模型文件中可以看到這個(gè)模型的基本構(gòu)成、卷積的尺寸和步長(zhǎng)等。

接下來(lái)將準(zhǔn)備使用的模型目錄拷貝到工程中。我們將MobileNet目錄的內(nèi)容放到assets/pml_demo下,以工程的src目錄為根目錄,展開(kāi)層級(jí)如下:

        src
        └── main
        ├── AndroidManifest.xml
        ├── assets
        │   └── pml_demo
        │        ├── apple.jpg
        │        ├── banana.jpeg
        │        ├── hand.jpg
        │        ├── hand2.jpg
        │        └── mobilenet
        │            ├── __model__
        │            ├── conv1_biases
        │            ├── conv1_bn_mean
        │            ├── conv1_bn_offset
        │            ├── conv1_bn_scale
        │            ├── conv1_bn_variance
        │            ├── conv1_weights
        │            ├── conv2_1_dw_biases

將模型拷貝到目地位置后,就要開(kāi)始開(kāi)發(fā)App的相關(guān)功能了,圖1-20所示是筆者的App工程布局。

圖1-20 App工程布局示例

App啟動(dòng)后的第一件事是將模型文件從磁盤(pán)加載到內(nèi)存中,這個(gè)過(guò)程被封裝在ModelLoader中。在MainActivity中實(shí)現(xiàn)init初始化方法,在初始化過(guò)程中加載模型。經(jīng)過(guò)簡(jiǎn)化后的代碼如下所示(如需運(yùn)行完整代碼,請(qǐng)見(jiàn)“鏈接8”)。

        privatefun init() {
            updateCurrentModel()
            mModelLoader.setThreadCount(mThreadCounts)
            thread_counts.text = "$mThreadCounts"
            clearInfos()
            mCurrentPath = banana.absolutePath
            predict_banada.setOnClickListener {
              scaleImageAndPredictImage(mCurrentPath, mPredictCounts)
            }
            btn_takephoto.setOnClickListener {
              if(! isHasSdCard) {
                  Toast.makeText(this@MainActivity,
R.string.sdcard_not_available,
                          Toast.LENGTH_LONG).show()
                  return@setOnClickListener
              }
              takePicFromCamera()
            }
            bt_load.setOnClickListener {
              isloaded = true
              mModelLoader.load()
            }
            bt_clear.setOnClickListener {
              isloaded = false
              mModelLoader.clear()
              clearInfos()
            }
            ll_model.setOnClickListener {
              MaterialDialog.Builder(this)
                      .title("選擇模型")
                      .items(modelList)
                      .itemsCallbackSingleChoice(modelList.indexOf(mCurrentType))
                      { _, _, which, text ->
                          info { "which=$which" }
                          info { "text=$text" }
                          mCurrentType = modelList[which]
                          updateCurrentModel()
                          reloadModel()
                          clearInfos()
                          true
                      }
                      .positiveText("確定")
                      .show()
            }
            ll_threadcount.setOnClickListener {
                MaterialDialog.Builder(this)
                      .title("設(shè)置線程數(shù)量")
                      .items(threadCountList)
                      .itemsCallbackSingleChoice(threadCountList.indexOf(mThreadCo
  unts))
                      { _, _, which, _ ->
                          mThreadCounts = threadCountList[which]
                          info { "mThreadCounts=$mThreadCounts" }
                          mModelLoader.setThreadCount(mThreadCounts)
                          reloadModel()
                          thread_counts.text = "$mThreadCounts"
                          clearInfos()
                          true
                      }
                      .positiveText("確定")
                      .show()
            }
            runcount_counts.text = "$mPredictCounts"
            ll_runcount.setOnClickListener {
                MaterialDialog.Builder(this)
                      .inputType(InputType.TYPE_CLASS_NUMBER)
                      .input("設(shè)置預(yù)測(cè)次數(shù)", "10") { _, input ->
                          mPredictCounts = input.toString().toLong()
                          info { "mRunCount=$mPredictCounts" }
                          mModelLoader.mTimes = mPredictCounts
                          reloadModel()
                          runcount_counts.text = "$mPredictCounts"
                      }.inputRange(1, 3)
                      .show()
            }
        }

MainActivity類(lèi)的代碼也從側(cè)面反映了一個(gè)視覺(jué)深度學(xué)習(xí)App需要處理的一些問(wèn)題,比如與圖像相關(guān)的權(quán)限、輸入尺寸等問(wèn)題,可以從初始化等核心方法入手。從上面代碼中能看到MainActivity類(lèi)中的init方法實(shí)現(xiàn),init方法邏輯包含Loader的初始處理和一些基本事件的監(jiān)聽(tīng)。由于深度學(xué)習(xí)技術(shù)對(duì)算力要求較高,所以往往會(huì)利用多線程處理技術(shù)來(lái)提升性能,這里的init方法就調(diào)用了多線程處理過(guò)程。多線程相關(guān)的底層實(shí)現(xiàn)使用了openmp api,多線程邏輯相對(duì)簡(jiǎn)單地作為入口參數(shù)傳入其中。

MainActivity作為界面和調(diào)起入口角色,除了要負(fù)責(zé)init初始化任務(wù),還要負(fù)責(zé)調(diào)起邏輯。下面就是其調(diào)起預(yù)處理和深度學(xué)習(xí)預(yù)測(cè)過(guò)程的代碼。

        /**
         * 縮放,然后預(yù)測(cè)這張圖片
         */
        private funscaleImageAndPredictImage(path: String? , times: Long) {
            if(path == null) {
              Toast.makeText(this, "圖片lost", Toast.LENGTH_SHORT).show()
              return
            }
            if(mModelLoader.isbusy) {
              Toast.makeText(this, "處于前一次操作中", Toast.LENGTH_SHORT).show()
              return
            }
            mModelLoader.clearTimeList()
            tv_infos.text = "預(yù)處理數(shù)據(jù),執(zhí)行運(yùn)算..."
            mModelLoader.predictTimes(times)
            Observable
                  .just(path)
                  .map {
                      if(! isloaded) {
                          isloaded = true
                          mModelLoader.setThreadCount(mThreadCounts)
                          mModelLoader.load()
                      }
                      mModelLoader.getScaleBitmap(
                            this@MainActivity,
                            path
                      )
                  }
                  .subscribeOn(Schedulers.io())
                  .observeOn(AndroidSchedulers.mainThread())
                  .doOnNext { bitmap -> show_image.setImageBitmap(bitmap) }
                  .map { bitmap ->
                      varfloatsTen: FloatArray? = null
                      for(i in0..(times - 1)) {
                          valfloats = mModelLoader.predictImage(bitmap)
                          valpredictImageTime = mModelLoader.predictImageTime
                          mModelLoader.timeList.add(predictImageTime)
                          if(i == times / 2) {
                            floatsTen = floats
                          }
                      }
                      Pair(floatsTen! ! , bitmap)
                  }
                  .observeOn(AndroidSchedulers.mainThread())
                  .map { floatArrayBitmapPair ->
                      mModelLoader.mixResult(show_image, floatArrayBitmapPair)
                      floatArrayBitmapPair.second
                      floatArrayBitmapPair.first
                  }
                  .observeOn(Schedulers.io())
                  .map(mModelLoader::processInfo)
                  .observeOn(AndroidSchedulers.mainThread())
                  .subscribe(object: Observer<String? > {
                      override funonSubscribe(d: Disposable) {
                          mModelLoader.isbusy = true
                      }

                      override funonNext(resultInfo: String) {
                          tv_infomain.text = mModelLoader.getMainMsg()
                          tv_preinfos.text =
                                mModelLoader.getDebugInfo() + "\n" +
                                        mModelLoader.timeInfo + "\n" +
                                        "點(diǎn)擊查看結(jié)果"

                          tv_preinfos.setOnClickListener {
                            MaterialDialog.Builder(this@MainActivity)
                                    .title("結(jié)果:")
                                    .content(resultInfo)
                                    .show()
                      }
                          }

                      override funonComplete() {
                          mModelLoader.isbusy = false
                          tv_infos.text = ""
                      }

                      override funonError(e: Throwable) {
                          mModelLoader.isbusy = false
                      }
                  })
        }

多數(shù)情況下,深度學(xué)習(xí)程序要有預(yù)處理過(guò)程,目的是將輸入尺寸和格式規(guī)則化,視覺(jué)深度學(xué)習(xí)的處理過(guò)程也不例外。如果不是可變輸入的網(wǎng)絡(luò)結(jié)構(gòu),那么一張輸入圖片在進(jìn)入神經(jīng)網(wǎng)絡(luò)計(jì)算之前需要經(jīng)歷一些“整形”,這樣能讓輸入尺寸符合預(yù)期。下面來(lái)看一下包含主要計(jì)算邏輯的Loader,它包含預(yù)處理、預(yù)測(cè)等邏輯的直接實(shí)現(xiàn)。圖像本身的數(shù)據(jù)是一個(gè)矩陣,因而預(yù)處理邏輯往往也是以矩陣的方式來(lái)處理的。

        override fungetScaledMatrix(bitmap: Bitmap, desWidth: Int, desHeight: Int):
FloatArray {
            valrsGsBs = getRsGsBs(bitmap, desWidth, desHeight)

            valrs = rsGsBs.first
            valgs = rsGsBs.second
            valbs = rsGsBs.third

            valdataBuf = FloatArray(3 * desWidth * desHeight)

            if(rs.size + gs.size + bs.size ! = dataBuf.size) {
              throwIllegalArgumentException("rs.size + gs.size + bs.size ! = dataBuf.
size should equal")
            }

            // bbbb... gggg.... rrrr...
            for(i indataBuf.indices) {
              dataBuf[i] = when{
                  i < bs.size -> (bs[i] - means[0]) * scale
                  i < bs.size + gs.size -> (gs[i - bs.size] - means[1]) * scale
                  else-> (rs[i - bs.size - gs.size] - means[2]) * scale
              }
          }

          returndataBuf
        }

從上面的代碼也能看到,這個(gè)預(yù)處理過(guò)程結(jié)束后得到的是一個(gè)BGR(藍(lán)、綠、紅)格式的數(shù)組。這部分代碼在MobileNetModelLoaderImpl類(lèi)中可以找到(完整代碼見(jiàn)“鏈接9”)。

前面編譯了Paddle-Lite的so庫(kù),它是使用C++編寫(xiě)的工程?,F(xiàn)在我們要在Android App中使用相關(guān)so庫(kù)中的功能,需要通過(guò)JNI(Java Native Interface)調(diào)用Paddle-Lite庫(kù)函數(shù),將數(shù)據(jù)從Kotlin層傳入JNI,得到預(yù)測(cè)結(jié)構(gòu),如下面的代碼所示。

        override funpredictImage(inputBuf: FloatArray): FloatArray? {
            varpredictImage: FloatArray? = null
            try{
              valstart = System.currentTimeMillis()
              predictImage = PML.predictImage(inputBuf, ddims)
              valend = System.currentTimeMillis()
              predictImageTime = end - start
            } catch(e: Exception) {
            }
            returnpredictImage
        }

        override funpredictImage(bitmap: Bitmap): FloatArray? {
            returnpredictImage(getScaledMatrix(bitmap, getInputSize(), getInputSize()))
        }

從上述代碼可以看出,如果基于Paddle-Lite使用層面編寫(xiě)深度學(xué)習(xí)App,那么思路并不復(fù)雜。從MobileNetModelLoaderImpl類(lèi)中可以看到,核心調(diào)用過(guò)程的代碼量也非常少。

上述代碼省略了文件拷貝和其他一些預(yù)處理過(guò)程,只展示了核心處理過(guò)程。從中可以看到,使用已有的深度學(xué)習(xí)庫(kù)集成并開(kāi)發(fā)深度學(xué)習(xí)功能是比較簡(jiǎn)單的。源代碼在GitHub相應(yīng)庫(kù)中(見(jiàn)“鏈接10”),使用Android Stuido直接運(yùn)行,就能看到圖1-21所示的效果,Demo App對(duì)香蕉圖片正確分類(lèi),并輸出了相應(yīng)的文本。

圖1-21 使用Paddle-Lite框架實(shí)現(xiàn)的Demo運(yùn)行效果

主站蜘蛛池模板: 宣汉县| 肇州县| 呈贡县| 九江市| 文昌市| 新闻| 天气| 景宁| 黔东| 剑川县| 平罗县| 离岛区| 忻州市| 卢湾区| 平和县| 安远县| 金湖县| 教育| 六盘水市| 随州市| 永福县| 抚松县| 邯郸市| 锡林浩特市| 南陵县| 民勤县| 维西| 碌曲县| 柘荣县| 济阳县| 阿克苏市| 日喀则市| 政和县| 个旧市| 肇源县| 淅川县| 屯门区| 长乐市| 高陵县| 铜山县| 鹰潭市|