知乎企業(yè)網(wǎng)站建設(shè)免費(fèi)網(wǎng)絡(luò)項(xiàng)目資源網(wǎng)
原文:Mobile Deep Learning with TensorFlow Lite, ML Kit and Flutter
協(xié)議:CC BY-NC-SA 4.0
譯者:飛龍
本文來自【ApacheCN 深度學(xué)習(xí) 譯文集】,采用譯后編輯(MTPE)流程來盡可能提升效率。
不要擔(dān)心自己的形象,只關(guān)心如何實(shí)現(xiàn)目標(biāo)?!对瓌t》,生活原則 2.3.c
六、構(gòu)建人工智能認(rèn)證系統(tǒng)
認(rèn)證是任何應(yīng)用中最突出的功能之一,無論它是本機(jī)移動軟件還是網(wǎng)站,并且自從保護(hù)數(shù)據(jù)的需求以及與機(jī)密有關(guān)的隱私需求開始以來,認(rèn)證一直是一個活躍的領(lǐng)域。 在互聯(lián)網(wǎng)上共享的數(shù)據(jù)。 在本章中,我們將從基于 Firebase 的簡單登錄到應(yīng)用開始,然后逐步改進(jìn)以包括基于人工智能(AI)的認(rèn)證置信度指標(biāo)和 Google 的 ReCaptcha。 所有這些認(rèn)證方法均以深度學(xué)習(xí)為核心,并提供了一種在移動應(yīng)用中實(shí)現(xiàn)安全性的最新方法。
在本章中,我們將介紹以下主題:
- 一個簡單的登錄應(yīng)用
- 添加 Firebase 認(rèn)證
- 了解用于認(rèn)證的異常檢測
- 用于認(rèn)證用戶的自定義模型
- 實(shí)現(xiàn) ReCaptcha 來避免垃圾郵件
- 在 Flutter 中部署模型
技術(shù)要求
對于移動應(yīng)用,需要具有 Flutter 的 Visual Studio Code 和 Dart 插件以及 Firebase Console
GitHub 網(wǎng)址。
一個簡單的登錄應(yīng)用
我們將首先創(chuàng)建一個簡單的認(rèn)證應(yīng)用,該應(yīng)用使用 Firebase 認(rèn)證對用戶進(jìn)行認(rèn)證,然后再允許他們進(jìn)入主屏幕。 該應(yīng)用將允許用戶輸入其電子郵件和密碼來創(chuàng)建一個帳戶,然后使他們隨后可以使用此電子郵件和密碼登錄。
以下屏幕快照顯示了應(yīng)用的完整流程:
該應(yīng)用的小部件樹如下:
現(xiàn)在讓我們詳細(xì)討論每個小部件的實(shí)現(xiàn)。
創(chuàng)建 UI
讓我們從創(chuàng)建應(yīng)用的登錄屏幕開始。 用戶界面(UI)將包含兩個TextFormField
來獲取用戶的電子郵件 ID 和密碼,RaisedButton
進(jìn)行注冊/登錄,以及FlatButton
進(jìn)行注冊和登錄操作之間的切換。
以下屏幕快照標(biāo)記了將用于應(yīng)用的第一個屏幕的小部件:
現(xiàn)在讓我們創(chuàng)建應(yīng)用的 UI,如下所示:
- 我們首先創(chuàng)建一個名為
signup_signin_screen.dart
的新 dart 文件。 該文件包含一個有狀態(tài)的小部件–SignupSigninScreen
。 - 第一個屏幕中最上面的窗口小部件是
TextField
,用于獲取用戶的郵件 ID。_createUserMailInput()
方法可幫助我們構(gòu)建窗口小部件:
Widget _createUserMailInput() {return Padding(padding: const EdgeInsets.fromLTRB(0.0, 100.0, 0.0, 0.0),child: new TextFormField(maxLines: 1,keyboardType: TextInputType.emailAddress,autofocus: false,decoration: new InputDecoration(hintText: 'Email',icon: new Icon(Icons.mail,color: Colors.grey,)),validator: (value) => value.isEmpty ? 'Email can\'t be empty' : null,onSaved: (value) => _usermail = value.trim(),),);}
首先,我們使用EdgeInsets.fromLTRB()
為小部件提供了填充。 這有助于我們在四個基本方向的每個方向(即左,上,右和下)上創(chuàng)建具有不同值的偏移量。 接下來,我們使用maxLines
(輸入的最大行數(shù))創(chuàng)建了TextFormField
,其值為1
作為子級,它接收用戶的電子郵件地址。 另外,根據(jù)輸入類型TextInputType.emailAddress
,我們指定了將在屬性keyboardType
中使用的鍵盤類型。 然后,將autoFocus
設(shè)置為false
。 然后,我們在裝飾屬性中使用InputDecoration
提供hintText "Email"
和圖標(biāo)Icons.mail
。 為了確保用戶在沒有輸入電子郵件地址或密碼的情況下不要嘗試登錄,我們添加了一個驗(yàn)證器。 當(dāng)嘗試使用空字段登錄時,將顯示警告“電子郵件不能為空”。 最后,我們通過使用trim()
刪除所有尾隨空格來修剪輸入的值,然后將輸入的值存儲在_usermail
字符串變量中。
- 與“步驟 2”中的
TextField
相似,我們定義了下一個方法_createPasswordInput()
,以創(chuàng)建用于輸入密碼的TextFormField()
:
Widget _createPasswordInput() {return Padding(padding: const EdgeInsets.fromLTRB(0.0, 15.0, 0.0, 0.0),child: new TextFormField(maxLines: 1,obscureText: true,autofocus: false,decoration: new InputDecoration(hintText: 'Password',icon: new Icon(Icons.lock,color: Colors.grey,)),validator: (value) => value.isEmpty ? 'Password can\'t be empty' : null,onSaved: (value) => _userpassword = value.trim(),),);}
我們首先使用EdgeInsets.fromLTRB()
在所有四個基本方向上提供填充,以在頂部提供15.0
的偏移量。 接下來,我們創(chuàng)建一個TextFormField
,其中maxLines
為1
,并將obscureText
設(shè)置為true
,將autofocus
設(shè)置為false
。 obscureText
用于隱藏正在鍵入的文本。 我們使用InputDecoration
提供hintText
密碼和一個灰色圖標(biāo)Icons.lock
。 為確保文本字段不為空,使用了一個驗(yàn)證器,當(dāng)傳遞空值時,該警告器會發(fā)出警告Password can't be empty
,即用戶嘗試在不輸入密碼的情況下登錄/注冊。 最后,trim()
用于刪除所有尾隨空格,并將密碼存儲在_userpassword
字符串變量中。
- 接下來,我們在
_SignupSigninScreenState
外部聲明FormMode
枚舉,該枚舉在兩種模式SIGNIN
和SIGNUP
之間運(yùn)行,如以下代碼片段所示:
enum FormMode { SIGNIN, SIGNUP }
我們將對該按鈕使用此枚舉,該按鈕將使用戶既可以登錄又可以注冊。 這將幫助我們輕松地在兩種模式之間切換。 枚舉是一組用于表示常量值的標(biāo)識符。
使用enum
關(guān)鍵字聲明枚舉類型。 在enum
內(nèi)部聲明的每個標(biāo)識符都代表一個整數(shù)值; 例如,第一標(biāo)識符具有值0
,第二標(biāo)識符具有值1
。 默認(rèn)情況下,第一個標(biāo)識符的值為0
。
- 讓我們定義一個
_createSigninButton()
方法,該方法返回按鈕小部件以使用戶注冊并登錄:
Widget _createSigninButton() {return new Padding(padding: EdgeInsets.fromLTRB(0.0, 45.0, 0.0, 0.0),child: SizedBox(height: 40.0,child: new RaisedButton(elevation: 5.0,shape: new RoundedRectangleBorder(borderRadius: new BorderRadius.circular(30.0)),color: Colors.blue,child: _formMode == FormMode.SIGNIN? new Text('SignIn',style: new TextStyle(fontSize: 20.0, color: Colors.white)): new Text('Create account',style: new TextStyle(fontSize: 20.0, color: Colors.white)),onPressed: _signinSignup,),));}
我們從Padding
開始,將45.0
的按鈕offset
置于頂部,然后將SizedBox
和40.0
的height
作為子項(xiàng),并將RaisedButton
作為其子項(xiàng)。 使用RoundedRectangleBorder()
為凸起的按鈕賦予圓角矩形形狀,其邊框半徑為30.0
,顏色為blue
。 作為子項(xiàng)添加的按鈕的文本取決于_formMode
的當(dāng)前值。 如果_formMode
的值(FormMode
枚舉的一個實(shí)例)為FormMode.SIGNIN
,則按鈕顯示SignIn
,否則創(chuàng)建帳戶。 按下按鈕時將調(diào)用_signinSignup
方法,該方法將在后面的部分中介紹。
- 現(xiàn)在,我們將第四個按鈕添加到屏幕上,以使用戶在
SIGNIN
和SIGNUP
表單模式之間切換。 我們定義返回FlatButton
的_createSigninSwitchButton()
方法,如下所示:
Widget _createSigninSwitchButton() {return new FlatButton(child: _formMode == FormMode.SIGNIN? new Text('Create an account',style: new TextStyle(fontSize: 18.0, fontWeight: FontWeight.w300)): new Text('Have an account? Sign in',style:new TextStyle(fontSize: 18.0, fontWeight: FontWeight.w300)),onPressed: _formMode == FormMode.SIGNIN? _switchFormToSignUp: _switchFormToSignin,);}
如果_formMode
的當(dāng)前值為SIGNIN
并按下按鈕,則應(yīng)更改為SIGNUP
并顯示Create an account
。 否則,如果_formMode
將SIGNUP
作為其當(dāng)前值,并且按下按鈕,則該值應(yīng)切換為由文本Have an account? Sign in
表示的SIGNIN
。 使用三元運(yùn)算符創(chuàng)建RaisedButton
的Text
子級時,添加了在文本之間切換的邏輯。 onPressed
屬性使用非常相似的邏輯,該邏輯再次檢查_formMode
的值以在模式之間切換并使用_switchFormToSignUp
和_switchFormToSignin
方法更新_formMode
的值。 我們將在“步驟 7”和 8 中定義_switchFormToSignUp
和_switchFormToSignin
方法。
- 現(xiàn)在,我們定義
_switchFormToSignUp()
如下:
void _switchFormToSignUp() {_formKey.currentState.reset();setState(() {_formMode = FormMode.SIGNUP;});}
此方法重置_formMode
的值并將其更新為FormMode.SIGNUP
。 更改setState()
內(nèi)部的值有助于通知框架該對象的內(nèi)部狀態(tài)已更改,并且 UI 可能需要更新。
- 我們以與
_switchFormToSignUp()
非常相似的方式定義_switchFormToSignin()
:
void _switchFormToSignin() {_formKey.currentState.reset();setState(() {_formMode = FormMode.SIGNIN;});}
此方法重置_formMode
的值并將其更新為FormMode.SIGNIN
。 更改setState()
內(nèi)部的值有助于通知框架該對象的內(nèi)部狀態(tài)已更改,并且 UI 可能需要更新。
- 現(xiàn)在,讓我們將所有屏幕小部件
Email TextField
,Password TextFied
,SignIn Button
和FlatButton
切換為在單個容器中進(jìn)行注冊和登錄。 為此,我們定義了一種方法createBody()
,如下所示:
Widget _createBody(){return new Container(padding: EdgeInsets.all(16.0),child: new Form(key: _formKey,child: new ListView(shrinkWrap: true,children: <Widget>[_createUserMailInput(),_createPasswordInput(),_createSigninButton(),_createSigninSwitchButton(),_createErrorMessage(),],),));}
此方法返回一個以Form
作為子元素的新Container
并為其填充16.0
。 表單使用_formKey
作為其鍵,并添加ListView
作為其子級。 ListView
的元素是我們在前述方法中創(chuàng)建的用于添加TextFormFields
和Buttons
的小部件。 shrinkWrap
設(shè)置為true
,以確保ListView
僅占用必要的空間,并且不會嘗試擴(kuò)展和填充整個屏幕
Form
類用于將多個FormFields
一起分組和驗(yàn)證。 在這里,我們使用Form
將兩個TextFormFields
,一個RaisedButton
和一個FlatButton
包裝在一起。
- 這里要注意的一件事是,由于進(jìn)行認(rèn)證,因此用戶最終將成為網(wǎng)絡(luò)操作,因此可能需要一些時間來發(fā)出網(wǎng)絡(luò)請求。 在此處添加進(jìn)度條可防止在進(jìn)行網(wǎng)絡(luò)操作時 UI 的死鎖。 我們聲明
boolean
標(biāo)志_loading
,當(dāng)網(wǎng)絡(luò)操作開始時將其設(shè)置為true
。 現(xiàn)在,我們定義一種_createCircularProgress()
方法,如下所示:
Widget _createCircularProgress(){if (_loading) {return Center(child: CircularProgressIndicator());} return Container(height: 0.0, width: 0.0,);}
僅當(dāng)_loading
為true
并且正在進(jìn)行網(wǎng)絡(luò)操作時,該方法才返回CircularProgressIndicator()
。
- 最后,讓我們在
build()
方法內(nèi)添加所有小部件:
@overrideWidget build(BuildContext context) {return new Scaffold(appBar: new AppBar(title: new Text('Firebase Authentication'),),body: Stack(children: <Widget>[_createBody(),_createCircularProgress(),],));}
從build()
內(nèi)部,添加包含應(yīng)用標(biāo)題的AppBar
變量后,我們返回一個支架。 支架的主體包含一個帶有子項(xiàng)的棧,這些子項(xiàng)是_createBody()
和_createCircularProgress()
函數(shù)調(diào)用返回的小部件。
現(xiàn)在,我們已經(jīng)準(zhǔn)備好應(yīng)用的主要 UI 結(jié)構(gòu)。
可以在這個頁面中找到SignupSigninScreen
的完整代碼。
在下一部分中,我們將介紹將 Firebase 認(rèn)證添加到應(yīng)用中涉及的步驟。
添加 Firebase 認(rèn)證
如前所述,在“簡單登錄應(yīng)用”部分中,我們將使用用戶的電子郵件和密碼通過 Firebase 集成認(rèn)證。
要在 Firebase 控制臺上創(chuàng)建和配置 Firebase 項(xiàng)目,請參考“附錄”。
以下步驟詳細(xì)討論了如何在 Firebase Console 上設(shè)置項(xiàng)目:
- 我們首先在 Firebase 控制臺上選擇項(xiàng)目:
- 接下來,我們將在
Develop
菜單中單擊Authentication
選項(xiàng):
這將帶我們進(jìn)入認(rèn)證屏幕。
- 遷移到登錄標(biāo)簽并啟用登錄提供者下的“電子郵件/密碼”選項(xiàng):
這是設(shè)置 Firebase 控制臺所需的全部。
接下來,我們將 Firebase 集成到代碼中。 這樣做如下:
- 遷移到 Flutter SDK 中的項(xiàng)目,然后將
firebase-auth
添加到應(yīng)用級別build.gradle
文件中:
implementation 'com.google.firebase:firebase-auth:18.1.0'
- 為了使
FirebaseAuthentication
在應(yīng)用中正常工作,我們將在此處使用firebase_auth
插件。 在pubspec.yaml
文件的依賴項(xiàng)中添加插件依賴項(xiàng):
firebase_auth: 0.14.0+4
確保運(yùn)行flutter pub get
以安裝依賴項(xiàng)。
現(xiàn)在,讓我們編寫一些代碼以在應(yīng)用內(nèi)部提供 Firebase 認(rèn)證功能。
創(chuàng)建auth.dart
現(xiàn)在,我們將創(chuàng)建一個 Dart 文件auth.dart
。 該文件將作為訪問firebase_auth
插件提供的認(rèn)證方法的集中點(diǎn):
- 首先,導(dǎo)入
firebase_auth
插件:
import 'package:firebase_auth/firebase_auth.dart';
- 現(xiàn)在,創(chuàng)建一個抽象類
BaseAuth
,該類列出了所有認(rèn)證方法,并充當(dāng) UI 組件和認(rèn)證方法之間的中間層:
abstract class BaseAuth {Future<String> signIn(String email, String password);Future<String> signUp(String email, String password);Future<String> getCurrentUser();Future<void> signOut();
}
顧名思義,這些方法將使用認(rèn)證的四個主要函數(shù):
signIn()
:使用電子郵件和密碼登錄已經(jīng)存在的用戶signUp()
:使用電子郵件和密碼為新用戶創(chuàng)建帳戶getCurrentUser()
:獲取當(dāng)前登錄的用戶signOut()
:注銷已登錄的用戶
這里要注意的重要一件事是,由于這是網(wǎng)絡(luò)操作,因此所有方法都異步操作,并在執(zhí)行完成后返回Future
值。
- 創(chuàng)建一個實(shí)現(xiàn)
BaseAuth
的Auth
類:
class Auth implements BaseAuth {//. . . . .
}
在接下來的步驟中,我們將定義BaseAuth
中聲明的所有方法。
- 創(chuàng)建
FirebaseAuth
的實(shí)例:
final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
signIn()
方法實(shí)現(xiàn)如下:
Future<String> signIn(String email, String password) async {AuthResult result = await _firebaseAuth.signInWithEmailAndPassword(email: email, password: password);FirebaseUser user = result.user;return user.uid;
}
此方法接收用戶的電子郵件和密碼,然后調(diào)用signInWithEmailAndPassword()
,并傳遞電子郵件和密碼以登錄已經(jīng)存在的用戶。 登錄操作完成后,將返回AuthResult
實(shí)例。 我們將其存儲在result
中,還使用result.user
,它返回FirebaseUser.
。它可用于獲取與用戶有關(guān)的信息,例如他們的uid
,phoneNumber
和photoUrl
。 在這里,我們返回user.uid
,它是每個現(xiàn)有用戶的唯一標(biāo)識。 如前所述,由于這是網(wǎng)絡(luò)操作,因此它異步運(yùn)行,并在執(zhí)行完成后返回Future
。
- 接下來,我們將定義
signUp()
方法以添加新用戶:
Future<String> signUp(String email, String password) async {AuthResult result = await _firebaseAuth.createUserWithEmailAndPassword(email: email, password: password);FirebaseUser user = result.user;return user.uid;}
前面的方法接收在注冊過程中使用的電子郵件和密碼,并將其值傳遞給createUserWithEmailAndPassword
。 類似于上一步中定義的對象,此調(diào)用還返回AuthResult
對象,該對象還用于提取FirebaseUser
。 最后,signUp
方法返回新創(chuàng)建的用戶的uid
。
- 現(xiàn)在,我們將定義
getCurrentUser()
:
Future<String> getCurrentUser() async {FirebaseUser user = await _firebaseAuth.currentUser();return user.uid;}
在先前定義的函數(shù)中,我們使用_firebaseAuth.currentUser()
提取當(dāng)前登錄用戶的信息。 此方法返回包裝在FirebaseUser
對象中的完整信息。 我們將其存儲在user
變量中。 最后,我們使用user.uid
返回用戶的uid
。
- 接下來,我們執(zhí)行
signOut()
:
Future<void> signOut() async {return _firebaseAuth.signOut();}
此函數(shù)僅在當(dāng)前FirebaseAuth
實(shí)例上調(diào)用signOut()
并注銷已登錄的用戶。
至此,我們已經(jīng)完成了用于實(shí)現(xiàn) Firebase 認(rèn)證的所有基本編碼。
可以在這個頁面中查看auth.dart
中的整個代碼。
現(xiàn)在讓我們看看如何在應(yīng)用內(nèi)部使認(rèn)證生效。
在SignupSigninScreen
中添加認(rèn)證
在本節(jié)中,我們將在SignupSigninScreen
中添加 Firebase 認(rèn)證。
我們在signup_signin_screen.dart
文件中定義了_signinSignup()
方法。 當(dāng)按下登錄按鈕時,將調(diào)用該方法。 該方法的主體如下所示:
void _signinSignup() async {setState(() {_loading = true;});String userId = ""; if (_formMode == FormMode.SIGNIN) {userId = await widget.auth.signIn(_usermail, _userpassword);} else {userId = await widget.auth.signUp(_usermail, _userpassword);}setState(() {_loading = false;});if (userId.length > 0 && userId != null && _formMode == FormMode.SIGNIN) {widget.onSignedIn();}
}
在上述方法中,我們首先將_loading
的值設(shè)置為true
,以便進(jìn)度條顯示在屏幕上,直到登錄過程完成。 接下來,我們創(chuàng)建一個userId
字符串,一旦登錄/登錄操作完成,該字符串將存儲userId
的值。 現(xiàn)在,我們檢查_formMode
的當(dāng)前值。 如果等于FormMode.SIGNIN
,則用戶希望登錄到現(xiàn)有帳戶。 因此,我們使用傳遞到SignupSigninScreen
構(gòu)造器中的實(shí)例來調(diào)用Auth
類內(nèi)部定義的signIn()
方法。
這將在后面的部分中詳細(xì)討論。 否則,如果_formMode
的值等于FormMode.SIGNUP
,則將調(diào)用Auth
類的signUp()
方法,并傳遞用戶的郵件和密碼以創(chuàng)建新帳戶。 一旦成功完成登錄/注冊,userId
變量將用于存儲用戶的 ID。 整個過程完成后,將_loading
設(shè)置為false
,以從屏幕上刪除循環(huán)進(jìn)度指示器。 另外,如果在用戶登錄到現(xiàn)有帳戶時userId
具有有效值,則將調(diào)用onSignedIn()
,這會將用戶定向到應(yīng)用的主屏幕。
此方法也傳遞給SignupSigninScreen
的構(gòu)造器,并將在后面的部分中進(jìn)行討論。 最后,我們將整個主體包裹在try-catch
塊中,以便在登錄過程中發(fā)生的任何異常都可以捕獲而不會導(dǎo)致應(yīng)用崩潰,并可以在屏幕上顯示。
創(chuàng)建主屏幕
我們還需要確定認(rèn)證狀態(tài),即用戶在啟動應(yīng)用時是否已登錄,如果已經(jīng)登錄,則將其定向到主屏幕。如果尚未登錄,則應(yīng)顯示SignInSignupScreen
首先,在完成該過程之后,將啟動主屏幕。 為了實(shí)現(xiàn)這一點(diǎn),我們在新的 dart 文件main_screen.dart
中創(chuàng)建一個有狀態(tài)的小部件MainScreen
,然后執(zhí)行以下步驟:
- 我們將從定義枚舉
AuthStatus
開始,該枚舉表示用戶的當(dāng)前認(rèn)證狀態(tài),可以登錄或不登錄:
enum AuthStatus {NOT_SIGNED_IN,SIGNED_IN,
}
- 現(xiàn)在,我們創(chuàng)建
enum
類型的變量來存儲當(dāng)前認(rèn)證狀態(tài),其初始值設(shè)置為NOT_SIGNED_IN
:
AuthStatus authStatus = AuthStatus.NOT_SIGNED_IN;
- 初始化小部件后,我們將通過覆蓋
initState()
方法來確定用戶是否已登錄:
@overridevoid initState() {super.initState();widget.auth.getCurrentUser().then((user) {setState(() {if (user != null) {_userId = user;}authStatus =user == null ? AuthStatus.NOT_SIGNED_IN : AuthStatus.SIGNED_IN;});});}
使用在構(gòu)造器中傳遞的類的實(shí)例調(diào)用Auth
類的getCurrentUser()
。 如果該方法返回的值不為null
,則意味著用戶已經(jīng)登錄。因此,_userId
字符串變量的值設(shè)置為返回的值。 另外,將authStatus
設(shè)置為AuthStatus.SIGNED_IN.
,否則,如果返回的值為null
,則意味著沒有用戶登錄,因此authStatus
的值設(shè)置為AuthStatus.NOT_SIGNED_IN
。
- 現(xiàn)在,我們將定義另外兩個方法
onSignIn()
和onSignOut()
,以確保將認(rèn)證狀態(tài)正確存儲在變量中,并相應(yīng)地更新用戶界面:
void _onSignedIn() {widget.auth.getCurrentUser().then((user){setState(() {_userId = user;});});setState(() {authStatus = AuthStatus.SIGNED_IN;});}void _onSignedOut() {setState(() {authStatus = AuthStatus.NOT_SIGNED_IN;_userId = "";});}
_onSignedIn()
方法檢查用戶是否已經(jīng)登錄,并將authStatus
設(shè)置為AuthStatus.SIGNED_IN.
。 _onSignedOut()
方法檢查用戶是否已注銷,并將authStatus
設(shè)置為AuthStatus.SIGNED_OUT
。
- 最后,我們重寫
build
方法將用戶定向到正確的屏幕:
@overrideWidget build(BuildContext context) {if(authStatus == AuthStatus.SIGNED_OUT) {return new SignupSigninScreen(auth: widget.auth,onSignedIn: _onSignedIn,);} else {return new HomeScreen(userId: _userId,auth: widget.auth,onSignedOut: _onSignedOut,);}}
如果authStatus
為AuthStatus.SIGNED_OUT
,則返回SignupSigninScreen
,并傳遞auth
實(shí)例和_onSignedIn()
方法。 否則,將直接返回HomeScreen
,并傳遞已登錄用戶的userId
,Auth
實(shí)例類和_onSignedOut()
方法。
可以在此處查看main_screen.dart
的完整代碼。
在下一部分中,我們將為應(yīng)用添加一個非常簡單的主屏幕。
創(chuàng)建主屏幕
由于我們對認(rèn)證部分更感興趣,因此主屏幕(即成功登錄后指向用戶的屏幕)應(yīng)該非常簡單。 它僅包含一些文本和一個注銷選項(xiàng)。 正如我們對所有先前的屏幕和小部件所做的一樣,我們首先創(chuàng)建一個home_screen.dart
文件和一個有狀態(tài)的HomeScreen
小部件。
主屏幕將顯示如下:
此處的完整代碼位于重寫的build()
方法內(nèi)部:
@overrideWidget build(BuildContext context) {return new Scaffold(appBar: new AppBar(title: new Text('Firebase Authentication'),actions: <Widget>[new FlatButton(child: new Text('Logout',style: new TextStyle(fontSize: 16.0, color: Colors.white)),onPressed: _signOut)],),body: Center(child: new Text('Hello User', style: new TextStyle(fontSize: 32.0))),);}
我們在此處返回Scaffold
,其中包含標(biāo)題為Text Firebase Authentication
的AppBar
和actions
屬性的小部件列表。 actions
用于在應(yīng)用標(biāo)題旁邊添加小部件列表到應(yīng)用欄中。 在這里,它僅包含FlatButton
,Logout
,在按下時將調(diào)用_signOut
。
_signOut()
方法顯示如下:
_signOut() async {try {await widget.auth.signOut();widget.onSignedOut();} catch (e) {print(e);}}
該方法主要是調(diào)用Auth
類中定義的signOut()
方法,以將用戶從應(yīng)用中注銷。 回憶傳入HomeScreen
的MainScreen
的_onSignedOut()
方法。 當(dāng)用戶退出時,該方法在此處用作widget.onSignedOut()
來將authStatus
更改為SIGNED_OUT
。 同樣,它包裝在try-catch
塊中,以捕獲并打印此處可能發(fā)生的任何異常。
可以在此處查看home_screen.dart
的整個代碼。
至此,應(yīng)用的主要組件已經(jīng)準(zhǔn)備就緒,現(xiàn)在讓我們創(chuàng)建最終的材質(zhì)應(yīng)用。
創(chuàng)建main.dart
在main.dart
內(nèi)部,我們創(chuàng)建Stateless Widget
,App
,并覆蓋build()
方法,如下所示:
@overrideWidget build(BuildContext context) {return new MaterialApp(title: 'Firebase Authentication',debugShowCheckedModeBanner: false,theme: new ThemeData(primarySwatch: Colors.blue,),home: new MainScreen(auth: new Auth()));}
該方法從主屏幕返回MaterialApp
,以提供標(biāo)題,主題。
可以在此處查看main.dart
文件。
了解用于認(rèn)證的異常檢測
異常檢測是機(jī)器學(xué)習(xí)的一個備受關(guān)注的分支。 該術(shù)語含義簡單。 基本上,它是用于檢測異常的方法的集合。 想象一袋蘋果。 識別并挑選壞蘋果將是異常檢測的行為。
異常檢測以幾種方式執(zhí)行:
- 通過使用列的最小最大范圍來識別數(shù)據(jù)集中與其余樣本非常不同的數(shù)據(jù)樣本
- 通過將數(shù)據(jù)繪制為線形圖并識別圖中的突然尖峰
- 通過圍繞高斯曲線繪制數(shù)據(jù)并將最末端的點(diǎn)標(biāo)記為離群值(異常)
一些常用的方法是支持向量機(jī),貝葉斯網(wǎng)絡(luò)和 K 最近鄰。 在本節(jié)中,我們將重點(diǎn)介紹與安全性相關(guān)的異常檢測。
假設(shè)您通常在家中登錄應(yīng)用上的帳戶。 如果您突然從數(shù)千英里外的位置登錄帳戶,或者在另一種情況下,您以前從未使用過公共計(jì)算機(jī)登錄帳戶,那將是非??梢傻?#xff0c;但是突然有一天您這樣做。 另一個可疑的情況可能是您嘗試 10-20 次密碼,每次在成功成功登錄之前每次都輸入錯誤密碼。 當(dāng)您的帳戶遭到盜用時,所有這些情況都是可能的行為。 因此,重要的是要合并一個能夠確定您的常規(guī)行為并對異常行為進(jìn)行分類的系統(tǒng)。 換句話說,即使黑客使用了正確的密碼,企圖破壞您的帳戶的嘗試也應(yīng)標(biāo)記為異常。
這帶給我們一個有趣的觀點(diǎn),即確定用戶的常規(guī)行為。 我們?nèi)绾巫龅竭@一點(diǎn)? 什么是正常行為? 它是針對每個用戶的還是一個通用概念? 問題的答案是它是非常特定于用戶的。 但是,行為的某些方面對于所有用戶而言都可以相同。 一個應(yīng)用可能會在多個屏幕上啟動登錄。 單個用戶可能更喜歡其中一種或兩種方法。 這將導(dǎo)致特定于該用戶的特定于用戶的行為。 但是,如果嘗試從未由開發(fā)人員標(biāo)記為登錄屏幕的屏幕進(jìn)行登錄,則無論是哪個用戶嘗試登錄,都肯定是異常的。
在我們的應(yīng)用中,我們將集成一個這樣的系統(tǒng)。 為此,我們將記錄一段時間內(nèi)我們應(yīng)用的許多用戶進(jìn)行的所有登錄嘗試。 我們將特別注意他們嘗試登錄的屏幕以及它們傳遞給系統(tǒng)的數(shù)據(jù)類型。 一旦收集了很多這些樣本,就可以根據(jù)用戶執(zhí)行的任何操作來確定系統(tǒng)對認(rèn)證的信心。 如果系統(tǒng)在任何時候認(rèn)為用戶表現(xiàn)出的行為與他們的慣常行為相差很大,則該用戶將未經(jīng)認(rèn)證并被要求驗(yàn)證其帳戶詳細(xì)信息。
讓我們從創(chuàng)建預(yù)測模型開始,以確定用戶認(rèn)證是常規(guī)的還是異常的。
用于認(rèn)證用戶的自定義模型
我們將本節(jié)分為兩個主要子節(jié):
- 構(gòu)建用于認(rèn)證有效性檢查的模型
- 托管自定義認(rèn)證驗(yàn)證模型
讓我們從第一部分開始。
構(gòu)建用于認(rèn)證有效性檢查的模型
在本部分中,我們將構(gòu)建模型來確定是否有任何用戶正在執(zhí)行常規(guī)登錄或異常登錄:
- 我們首先導(dǎo)入必要的模塊,如下所示:
import sys
import os
import json
import pandas
import numpy
from keras.models import Sequential
from keras.layers import LSTM, Dense, Dropout
from keras.layers.embeddings import Embedding
from keras.preprocessing import sequence
from keras.preprocessing.text import Tokenizer
from collections import OrderedDict
- 現(xiàn)在,我們將數(shù)據(jù)集導(dǎo)入到項(xiàng)目中。 可以在這里中找到該數(shù)據(jù)集:
csv_file = 'data.csv'dataframe = pandas.read_csv(csv_file, engine='python', quotechar='|', header=None)
count_frame = dataframe.groupby([1]).count()
print(count_frame)
total_req = count_frame[0][0] + count_frame[0][1]
num_malicious = count_frame[0][1]print("Malicious request logs in dataset: {:0.2f}%".format(float(num_malicious) / total_req * 100))
前面的代碼塊將 CSV 數(shù)據(jù)集加載到項(xiàng)目中。 它還會打印一些與數(shù)據(jù)有關(guān)的統(tǒng)計(jì)信息,如下所示:
- 我們在上一步中加載的數(shù)據(jù)目前尚無法使用,無法進(jìn)行深度學(xué)習(xí)。 在此步驟中,我們將其分為特征列和標(biāo)簽列,如下所示:
X = dataset[:,0]
Y = dataset[:,1]
- 接下來,我們將刪除數(shù)據(jù)集中包含的某些列,因?yàn)槲覀儾恍枰羞@些列來構(gòu)建簡單的模型:
for index, item in enumerate(X):reqJson = json.loads(item, object_pairs_hook=OrderedDict)del reqJson['timestamp']del reqJson['headers']del reqJson['source']del reqJson['route']del reqJson['responsePayload']X[index] = json.dumps(reqJson, separators=(',', ':'))
- 接下來,我們將在剩余的請求正文上執(zhí)行分詞。 分詞是一種用于將大文本塊分解為較小文本的方法,例如將段落分成句子,將句子分成單詞。 我們這樣做如下:
tokenizer = Tokenizer(filters='\t\n', char_level=True)
tokenizer.fit_on_texts(X)
- 分詞之后,我們將請求正文中的文本轉(zhuǎn)換為單詞向量,如下一步所示。 我們將數(shù)據(jù)集和
DataFrame
標(biāo)簽分為兩部分,即 75%-25%,以進(jìn)行訓(xùn)練和測試:
num_words = len(tokenizer.word_index)+1
X = tokenizer.texts_to_sequences(X)max_log_length = 1024
train_size = int(len(dataset) * .75)X_processed = sequence.pad_sequences(X, maxlen=max_log_length)
X_train, X_test = X_processed[0:train_size], X_processed[train_size:len(X_processed)]
Y_train, Y_test = Y[0:train_size], Y[train_size:len(Y)]
- 接下來,我們基于長短期記憶(LSTM)創(chuàng)建基于循環(huán)神經(jīng)網(wǎng)絡(luò)(RNN)的學(xué)習(xí)方法,來識別常規(guī)用戶行為。 將單詞嵌入添加到層中,以幫助維持單詞向量和單詞之間的關(guān)系:
model = Sequential()
model.add(Embedding(num_words, 32, input_length=max_log_length))
model.add(Dropout(0.5))
model.add(LSTM(64, recurrent_dropout=0.5))
model.add(Dropout(0.5))
model.add(Dense(1, activation='sigmoid'))
我們的輸出是單個神經(jīng)元,在正常登錄的情況下,該神經(jīng)元保存0
;在登錄異常的情況下,則保存1
。
- 現(xiàn)在,我們以精度作為度量標(biāo)準(zhǔn)編譯模型,而損失則作為二進(jìn)制交叉熵來計(jì)算:
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
print(model.summary())
- 現(xiàn)在,我們準(zhǔn)備進(jìn)行模型的訓(xùn)練:
model.fit(X_train, Y_train, validation_split=0.25, epochs=3, batch_size=128)
- 我們將快速檢查模型所達(dá)到的準(zhǔn)確率。 當(dāng)前模型的準(zhǔn)確率超過 96%:
score, acc = model.evaluate(X_test, Y_test, verbose=1, batch_size=128)
print("Model Accuracy: {:0.2f}%".format(acc * 100))
下面的屏幕快照顯示了前面代碼塊的輸出:
- 現(xiàn)在,我們保存模型權(quán)重和模型定義。 我們稍后將它們加載到 API 腳本中,以驗(yàn)證用戶的認(rèn)證:
model.save_weights('lstm-weights.h5')
model.save('lstm-model.h5')
現(xiàn)在,我們可以將認(rèn)證模型作為 API 進(jìn)行托管,我們將在下一部分中進(jìn)行演示。
托管自定義認(rèn)證驗(yàn)證模型
在本節(jié)中,我們將創(chuàng)建一個 API,用于在用戶向模型提交其登錄請求時對其進(jìn)行認(rèn)證。 請求標(biāo)頭將被解析為字符串,并且模型將使用它來預(yù)測登錄是否有效:
- 我們首先導(dǎo)入創(chuàng)建 API 服務(wù)器所需的模塊:
from sklearn.externals import joblib
from flask import Flask, request, jsonify
from string import digitsimport sys
import os
import json
import pandas
import numpy
import optparse
from keras.models import Sequential, load_model
from keras.preprocessing import sequence
from keras.preprocessing.text import Tokenizer
from collections import OrderedDict
- 現(xiàn)在,我們實(shí)例化一個
Flask
應(yīng)用對象。 我們還將從上一節(jié)“構(gòu)建用于認(rèn)證有效性檢查的模型”中加載保存的模型定義和模型權(quán)重。然后,我們重新編譯模型,并使用_make_predict_function( )
方法創(chuàng)建其預(yù)測方法,如以下步驟所示:
app = Flask(__name__)model = load_model('lstm-model.h5')
model.load_weights('lstm-weights.h5')
model.compile(loss = 'binary_crossentropy', optimizer = 'adam', metrics = ['accuracy'])
model._make_predict_function()
- 然后,我們創(chuàng)建一個
remove_digits()
函數(shù),該函數(shù)用于從提供給它的輸入中去除所有數(shù)字。 這將用于在將請求正文文本放入模型之前清除它:
def remove_digits(s: str) -> str:remove_digits = str.maketrans('', '', digits)res = s.translate(remove_digits)return res
- 接下來,我們將在 API 服務(wù)器中創(chuàng)建
/login
路由。 該路由由login()
方法處理,并響應(yīng)GET
和POST
請求方法。 正如我們對訓(xùn)練輸入所做的那樣,我們刪除了請求標(biāo)頭中的非必要部分。 這可以確保模型將對數(shù)據(jù)進(jìn)行預(yù)測,類似于對其進(jìn)行訓(xùn)練的數(shù)據(jù):
@app.route('/login', methods=['GET, POST'])
def login():req = dict(request.headers)item = {}item["method"] = str(request.method)item["query"] = str(request.query_string)item["path"] = str(request.path)item["statusCode"] = 200item["requestPayload"] = []## MORE CODE BELOW THIS LINE## MORE CODE ABOVE THIS LINEresponse = {'result': float(prediction[0][0])}return jsonify(response)
- 現(xiàn)在,我們將代碼添加到
login()
方法中,該方法將標(biāo)記請求正文并將其傳遞給模型以執(zhí)行有關(guān)登錄請求有效性的預(yù)測,如下所示:
@app.route('/login', methods=['GET, POST'])
def login():...## MORE CODE BELOW THIS LINEX = numpy.array([json.dumps(item)])log_entry = "store"tokenizer = Tokenizer(filters='\t\n', char_level=True)tokenizer.fit_on_texts(X)seq = tokenizer.texts_to_sequences([log_entry])max_log_length = 1024log_entry_processed = sequence.pad_sequences(seq, maxlen=max_log_length)prediction = model.predict(log_entry_processed)## MORE CODE ABOVE THIS LINE...
最后,應(yīng)用以 JSON 字符串的形式返回其對用戶進(jìn)行認(rèn)證的信心。
- 最后,我們使用
app
的run()
方法啟動服務(wù)器腳本:
if __name__ == '__main__':app.run(host='0.0.0.0', port=8000)
- 將此文件另存為
main.py
。 要開始執(zhí)行服務(wù)器,請打開一個新終端并使用以下命令:
python main.py
服務(wù)器監(jiān)聽其運(yùn)行系統(tǒng)的所有傳入 IP。 通過在0.0.0.0
IP 上運(yùn)行它,可以實(shí)現(xiàn)這一點(diǎn)。 如果我們希望稍后在基于云的服務(wù)器上部署腳本,則需要這樣做。 如果不指定0.0.0.0
主機(jī),則默認(rèn)情況下會使它監(jiān)聽127.0.0.1
,這不適合在公共服務(wù)器上進(jìn)行部署。 您可以在此處詳細(xì)了解這些地址之間的區(qū)別。
在下一節(jié)中,我們將看到如何將 ReCaptcha 集成到迄今為止在該項(xiàng)目中構(gòu)建的應(yīng)用中。 之后,我們將把本節(jié)中構(gòu)建的 API 集成到應(yīng)用中。
實(shí)現(xiàn) ReCaptcha 來保護(hù)垃圾郵件
為了為 Firebase 認(rèn)證增加另一層安全性,我們將使用 ReCaptcha。 這是 Google 所支持的一項(xiàng)測試,可幫助我們保護(hù)數(shù)據(jù)免受垃圾郵件和濫用行為的自動 bot 攻擊。 該測試很簡單,很容易被人類解決,但是卻阻礙了漫游器和惡意用戶的使用。
要了解有關(guān) ReCaptcha 及其用途的更多信息,請?jiān)L問這里。
ReCAPTCHA v2
在本節(jié)中,我們將把 ReCaptcha 版本 2 集成到我們的應(yīng)用中。 在此版本中,向用戶顯示一個簡單的復(fù)選框。 如果刻度變?yōu)榫G色,則表明用戶已通過驗(yàn)證。
另外,還可以向用戶提出挑戰(zhàn),以區(qū)分人和機(jī)器人。 這個挑戰(zhàn)很容易被人類解決。 他們要做的就是根據(jù)說明選擇一堆圖像。 使用 ReCaptcha 進(jìn)行認(rèn)證的傳統(tǒng)流程如下所示:
一旦用戶能夠驗(yàn)證其身份,他們就可以成功登錄。
獲取 API 密鑰
要在我們的應(yīng)用內(nèi)部使用 ReCaptcha,我們需要在reCAPTCHA
管理控制臺中注冊該應(yīng)用,并獲取站點(diǎn)密鑰和秘密密鑰。 為此,請?jiān)L問這里并注冊該應(yīng)用。 您將需要導(dǎo)航到“注冊新站點(diǎn)”部分,如以下屏幕截圖所示:
我們可以通過以下兩個簡單步驟來獲取 API 密鑰:
- 首先提供一個域名。 在這里,我們將在 reCAPTCHA v2 下選擇 reCAPTCHA Android。
- 選擇 Android 版本后,添加項(xiàng)目的包名稱。 正確填寫所有信息后,單擊“注冊”。
這將引導(dǎo)您到顯示站點(diǎn)密鑰和秘密密鑰的屏幕,如以下屏幕快照所示:
將站點(diǎn)密鑰和秘密密鑰復(fù)制并保存到安全位置。 我們將在編碼應(yīng)用時使用它們。
代碼整合
為了在我們的應(yīng)用中包含 ReCaptcha v2,我們將使用 Flutter 包flutter_recaptcha_v2
。 將flutter_recaptcha_v2:0.1.0
依賴項(xiàng)添加到pubspec.yaml
文件中,然后在終端中運(yùn)行flutter packages get
以獲取所需的依賴項(xiàng)。 以下步驟詳細(xì)討論了集成:
- 我們將代碼添加到
signup_signin_screen.dart
。 首先導(dǎo)入依賴項(xiàng):
import 'package:flutter_recaptcha_v2/flutter_recaptcha_v2.dart';
- 接下來,創(chuàng)建一個
RecaptchaV2Controller
實(shí)例:
RecaptchaV2Controller recaptchaV2Controller = RecaptchaV2Controller();
- reCAPTCHA 復(fù)選框?qū)⑻砑訛樾〔考?首先,讓我們定義一個返回小部件的
_createRecaptcha()
方法:
Widget _createRecaptcha() {return RecaptchaV2(apiKey: "Your Site Key here", apiSecret: "Your API Key here", controller: recaptchaV2Controller,onVerifiedError: (err){print(err);},onVerifiedSuccessfully: (success) {setState(() {if (success) {_signinSignup();} else {print('Failed to verify');}});},);}
在上述方法中,我們僅使用RecaptchaV2()
構(gòu)造器,即可為特定屬性指定值。 添加您先前在apiKey
和apiSecret
屬性中注冊時保存的站點(diǎn)密鑰和秘密密鑰。 我們使用先前為屬性控制器創(chuàng)建的recaptcha
控制器recaptchaV2Controller
的實(shí)例。 如果成功驗(yàn)證了用戶,則將調(diào)用_signinSignup()
方法以使用戶登錄。如果在驗(yàn)證期間發(fā)生錯誤,我們將打印錯誤。
- 現(xiàn)在,由于在用戶嘗試登錄時應(yīng)顯示
reCaptcha
,因此我們將createSigninButton()
中的登錄凸起按鈕的onPressed
屬性修改為recaptchaV2Controller
:
Widget _createSigninButton() {. . . . . . .return new Padding(. . . . . . .child: new RaisedButton(. . . . . . //Modify the onPressed propertyonPressed: recaptchaV2Controller.show))
}
- 最后,我們將
_createRecaptcha()
添加到build()
內(nèi)部的主體棧中:
@overrideWidget build(BuildContext context) {. . . . . . .return new Scaffold(. . . . . . .body: Stack(children: <Widget>[_createBody(),_createCircularProgress(),//Add reCAPTCHA Widget_createRecaptcha()],));}
這就是一切! 現(xiàn)在,我們具有比 Firebase 認(rèn)證更高的安全級別,可以保護(hù)應(yīng)用的數(shù)據(jù)免受自動機(jī)器人的攻擊。 現(xiàn)在讓我們看一下如何集成定制模型以檢測惡意用戶。
在 Flutter 中部署模型
至此,我們的 Firebase 認(rèn)證應(yīng)用與 ReCaptcha 保護(hù)一起運(yùn)行。 現(xiàn)在,讓我們添加最后的安全層,該層將不允許任何惡意用戶進(jìn)入應(yīng)用。
我們已經(jīng)知道該模型位于以下端點(diǎn)。 我們只需從應(yīng)用內(nèi)部進(jìn)行 API 調(diào)用,傳入用戶提供的電子郵件和密碼,并從模型中獲取結(jié)果值。 該值將通過使用閾值結(jié)果值來幫助我們判斷登錄是否是惡意的。
如果該值小于 0.20,則認(rèn)為該登錄名是惡意的,并且屏幕上將顯示以下消息:
現(xiàn)在,讓我們看一下在 Flutter 應(yīng)用中部署模型的步驟:
- 首先,由于我們正在獲取數(shù)據(jù)并且將使用網(wǎng)絡(luò)調(diào)用(即 HTTP 請求),因此我們需要向
pubspec.yaml
文件添加http
依賴項(xiàng),并按以下方式導(dǎo)入:
import 'package:http/http.dart' as http;
- 首先在
auth.dart:
內(nèi)部定義的BaseAuth
抽象類中添加以下函數(shù)聲明
Future<double> isValidUser(String email, String password);
- 現(xiàn)在,讓我們在
Auth
類中定義isValidUser()
函數(shù):
Future<double> isValidUser(String email, String password) async{final response = await http.Client().get('http://34.67.160.232:8000/login?user=$email&password=$password');var jsonResponse = json.decode(response.body);var val = '${jsonResponse["result"]}';double result = double.parse(val); return result;}
此函數(shù)將用戶的電子郵件和密碼作為參數(shù),并將它們附加到請求 URL,以便為特定用戶生成輸出。 get request
響應(yīng)存儲在變量響應(yīng)中。 由于響應(yīng)為 JSON 格式,因此我們使用json.decode()
對其進(jìn)行解碼,并將解碼后的響應(yīng)存儲在另一個變量響應(yīng)中。 現(xiàn)在,我們使用‘${jsonResponse["result"]}'
訪問jsonResponse
中的結(jié)果值,使用double.parse()
將其轉(zhuǎn)換為雙精度類型整數(shù),并將其存儲在結(jié)果中。 最后,我們返回結(jié)果的值。
- 為了激活代碼內(nèi)部的惡意檢測,我們從
SigninSignupScreen
調(diào)用了isValidUser()
方法。 當(dāng)具有現(xiàn)有帳戶的用戶選擇從if-else
塊內(nèi)部登錄時,將調(diào)用此方法:
if (_formMode == FormMode.SIGNIN) {var val = await widget.auth.isValidUser(_usermail, _userpassword);. . . .} else {. . . . }
isValidUser
返回的值存儲在val
變量中。
- 如果該值小于 0.20,則表明登錄活動是惡意的。 因此,我們將異常拋出并在 catch 塊內(nèi)拋出
catch
并在屏幕上顯示錯誤消息。 這可以通過創(chuàng)建自定義異常類MalicousUserException
來完成,該類在實(shí)例化時返回一條錯誤消息:
class MaliciousUserException implements Exception {String message() => 'Malicious login! Please try later.';
}
- 現(xiàn)在,我們將在調(diào)用
isValidUser()
之后添加if
塊,以檢查是否需要拋出異常:
var val = await widget.auth.isValidUser(_usermail, _userpassword);
//Add the if block
if(val < 0.20) {throw new MaliciousUserException();
}
- 現(xiàn)在,該異常已捕獲在
catch
塊內(nèi),并且不允許用戶繼續(xù)登錄。此外,我們將_loading
設(shè)置為false
以表示不需要進(jìn)一步的網(wǎng)絡(luò)操作:
catch(MaliciousUserException) {setState(() {_loading = false;_errorMessage = 'Malicious user detected. Please try again later.';});
這就是一切! 我們之前基于 Firebase 認(rèn)證創(chuàng)建的 Flutter 應(yīng)用現(xiàn)在可以在后臺運(yùn)行智能模型的情況下找到惡意用戶。
總結(jié)
在本章中,我們了解了如何使用 Flutter 和由 Firebase 支持的認(rèn)證系統(tǒng)構(gòu)建跨平臺應(yīng)用,同時結(jié)合了深度學(xué)習(xí)的優(yōu)勢。 然后,我們了解了如何將黑客攻擊嘗試歸類為一般用戶行為中的異?,F(xiàn)象,并創(chuàng)建了一個模型來對這些異常現(xiàn)象進(jìn)行分類以防止惡意用戶登錄。最后,我們使用了 Google 的 ReCaptcha 來消除對該應(yīng)用的垃圾郵件使用,因此,使其在自動垃圾郵件或腳本化黑客攻擊方面更具彈性。
在下一章中,我們將探索一個非常有趣的項(xiàng)目–使用移動應(yīng)用上的深度學(xué)習(xí)生成音樂成績單。
七、語音/多媒體處理 - 使用 AI 生成音樂
鑒于人工智能(AI)的應(yīng)用越來越多,將 AI 與音樂結(jié)合使用的想法已經(jīng)存在了很長時間,并且受到了廣泛的研究。 由于音樂是一系列音符,因此它是時間序列數(shù)據(jù)集的經(jīng)典示例。 最近證明時間序列數(shù)據(jù)集在許多預(yù)測領(lǐng)域中非常有用–股市,天氣模式,銷售模式以及其他基于時間的數(shù)據(jù)集。 循環(huán)神經(jīng)網(wǎng)絡(luò)(RNN)是處理時間序列數(shù)據(jù)集的最多模型之一。 對 RNN 進(jìn)行的流行增強(qiáng)稱為長短期記憶(LSTM)神經(jīng)元。 在本章中,我們將使用 LSTM 處理音符。
多媒體處理也不是一個新話題。 在本項(xiàng)目系列的早期,我們在多章中詳細(xì)介紹了圖像處理。 在本章中,我們將討論并超越圖像處理,并提供一個帶有音頻的深度學(xué)習(xí)示例。 我們將訓(xùn)練 Keras 模型來生成音樂樣本,每次都會生成一個新樣本。 然后,我們將此模型與 Flutter 應(yīng)用結(jié)合使用,以通過 Android 和 iOS 設(shè)備上的音頻播放器進(jìn)行部署。
在本章中,我們將介紹以下主題:
- 設(shè)計(jì)項(xiàng)目的架構(gòu)
- 了解多媒體處理
- 開發(fā)基于 RNN 的音樂生成模型
- 在 Android 和 iOS 上部署音頻生成 API
讓我們首先概述該項(xiàng)目的架構(gòu)。
設(shè)計(jì)項(xiàng)目的架構(gòu)
該項(xiàng)目的架構(gòu)與作為應(yīng)用部署的常規(guī)深度學(xué)習(xí)項(xiàng)目略有不同。 我們將有兩組不同的音樂樣本。 第一組樣本將用于訓(xùn)練可以生成音樂的 LSTM 模型。 另一組樣本將用作 LSTM 模型的隨機(jī)輸入,該模型將輸出生成的音樂樣本。 我們稍后將開發(fā)和使用的基于 LSTM 的模型將部署在 Google Cloud Platform(GCP)上。 但是,您可以將其部署在 AWS 或您選擇的任何其他主機(jī)上。
下圖總結(jié)了將在本項(xiàng)目中使用的不同組件之間的交互:
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-pw7KUWRD-1681785128417)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mobi-dl-tflite/img/50f17dc4-2658-4211-a7ff-9c41daafd884.png)]
移動應(yīng)用要求部署在服務(wù)器上的模型生成新的音樂樣本。 該模型使用隨機(jī)音樂樣本作為輸入,以使其通過預(yù)先訓(xùn)練的模型來生成新的音樂樣本。 然后,新的音樂樣本由移動設(shè)備獲取并播放給用戶。
您可以將此架構(gòu)與我們之前介紹的架構(gòu)進(jìn)行比較,在該架構(gòu)中,將有一組用于訓(xùn)練的數(shù)據(jù)樣本,然后將模型部署在云上或本地,并用于作出預(yù)測。
我們還可以更改此項(xiàng)目架構(gòu),以在存在為 Dart 語言編寫的 midi 文件處理庫的情況下在本地部署模型。 但是,在撰寫本文時,還沒有與我們在開發(fā)模型時使用的 Python midi 文件庫的要求兼容的穩(wěn)定庫。
讓我們從學(xué)習(xí)多媒體處理的含義以及如何使用 OpenCV 處理多媒體文件開始。
了解多媒體處理
多媒體是幾乎所有形式的視覺,聽覺或兩者兼有的內(nèi)容的總稱。 術(shù)語多媒體處理本身非常模糊。 討論該術(shù)語的更精確方法是將其分解為兩個基本部分-視覺或聽覺。 因此,我們將討論多媒體處理的術(shù)語,即圖像處理和音頻處理。 這些術(shù)語的混合產(chǎn)生了視頻處理,這只是多媒體的另一種形式。
在以下各節(jié)中,我們將以單獨(dú)的形式討論它們。
圖像處理
圖像處理或計(jì)算機(jī)視覺是迄今為止人工智能研究最多的分支之一。 在過去的幾十年中,它發(fā)展迅速,并在以下幾種技術(shù)的進(jìn)步中發(fā)揮了重要作用:
- 圖像過濾器和編輯器
- 面部識別
- 數(shù)字繪畫
- 自動駕駛汽車
我們在較早的項(xiàng)目中討論了圖像處理的基礎(chǔ)知識。 在這個項(xiàng)目中,我們將討論一個非常流行的用于執(zhí)行圖像處理的庫-OpenCV。 OpenCV 是開源計(jì)算機(jī)視覺的縮寫。 它由 Intel 開發(fā),并由 Willow Garage 和 Itseez(后來被 Intel 收購)推動。 毫無疑問,由于它與所有主要的機(jī)器學(xué)習(xí)框架(例如 TensorFlow,PyTorch 和 Caffe)兼容,因此它是執(zhí)行圖像處理的全球大多數(shù)開發(fā)人員的首要選擇。 除此之外,OpenCV 還可以使用多種語言,例如 C++,Java 和 Python。
要在 Python 環(huán)境中安裝 OpenCV,可以使用以下命令:
pip install opencv-contrib-python
前面的命令將同時安裝主 OpenCV 模塊和contrib
模塊。 您可以在此處找到更多模塊供您選擇。 有關(guān)更多安裝說明,如果前面的鏈接不符合您的要求,則可以在此處遵循官方文檔。
讓我們?yōu)槟榻B一個非常簡單的示例,說明如何使用 OpenCV 執(zhí)行圖像處理。 創(chuàng)建一個新的 Jupyter 筆記本,并從以下步驟開始:
- 要將 OpenCV 導(dǎo)入筆記本,請使用以下代碼行:
import cv2
- 我們還要將 matplotlib 導(dǎo)入筆記本,因?yàn)槿绻鷩L試使用本機(jī) OpenCV 圖像顯示功能,Jupyter 筆記本將會崩潰:
from matplotlib import pyplot as plt
%matplotlib inline
- 讓我們使用 matplotlib 為 OpenCV 的本機(jī)圖像顯示功能創(chuàng)建一個替代函數(shù),以方便在筆記本中顯示圖像:
def showim(image):image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)plt.imshow(image)plt.show()
請注意,我們將圖像的配色方案從藍(lán)色綠色紅色(BGR)轉(zhuǎn)換為紅色綠色藍(lán)色(RGB)。 這是由于默認(rèn)情況下 OpenCV 使用 BGR 配色方案。 但是,matplotlib 在顯示圖片時會使用 RGB 方案,并且如果不進(jìn)行這種轉(zhuǎn)換,我們的圖像就會顯得奇怪。
- 現(xiàn)在,讓我們將圖像讀取到 Jupyter 筆記本中。 完成后,我們將能夠看到加載的圖像:
image = cv2.imread("Image.jpeg")
showim(image)
前面代碼的輸出取決于您選擇加載到筆記本中的圖像:
在我們的示例中,我們加載了柑橘類水果切片的圖像,這是艾薩克·奎薩達(dá)(Isaac Quesada)在“Unsplash”上拍攝的驚人照片。
您可以在這里找到上一張圖片。
- 讓我們通過將之前的圖像轉(zhuǎn)換為灰度圖像來進(jìn)行簡單的操作。 為此,我們就像在聲明的
showim()
函數(shù)中那樣簡單地使用轉(zhuǎn)換方法:
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
showim(gray_image)
這將產(chǎn)生以下輸出:
- 現(xiàn)在讓我們執(zhí)行另一種常見的操作,即圖像模糊。 在圖像處理中通常采用模糊處理,以消除圖像中信息的不必要的細(xì)節(jié)(此時)。 我們使用高斯模糊過濾器,這是在圖像上創(chuàng)建模糊的最常見算法之一:
blurred_image = cv2.GaussianBlur(image, (7, 7), 0)
showim(blurred_image)
這將產(chǎn)生以下輸出:
請注意,前面的圖像不如原始圖像清晰。 但是,它很容易達(dá)到愿意計(jì)算此圖像中對象數(shù)量的目的。
- 為了在圖像中定位對象,我們首先需要標(biāo)記圖像中的邊緣。 為此,我們可以使用
Canny()
方法,該方法是 OpenCV 中可用的其他選項(xiàng)之一,用于查找圖像的邊緣:
canny = cv2.Canny(blurred_image, 10, 50)
showim(canny)
這將產(chǎn)生以下輸出:
請注意,在上圖中找到的邊緣數(shù)量很高。 雖然這會顯示圖像的細(xì)節(jié),但是如果我們嘗試對邊緣進(jìn)行計(jì)數(shù)以嘗試確定圖像中的對象數(shù)量,這將無濟(jì)于事。
- 讓我們嘗試計(jì)算上一步生成的圖像中不同項(xiàng)目的數(shù)量:
contours, hierarchy= cv2.findContours(canny, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
print("Number of objects found = ", len(contours))
上面的代碼將產(chǎn)生以下輸出:
Number of objects found = 18
但是,我們知道前面的圖像中沒有 18 個對象。 只有 9。因此,在尋找邊緣時,我們將在canny
方法中處理閾值。
- 讓我們在 canny 方法中增加邊緣發(fā)現(xiàn)的閾值。 這使得更難檢測到邊緣,因此僅使最明顯的邊緣可見:
canny = cv2.Canny(blurred_image, 50, 150)
showim(canny)
這將產(chǎn)生以下輸出:
請注意,在柑橘類水果體內(nèi)發(fā)現(xiàn)的邊緣急劇減少,僅清晰可見其輪廓。 我們希望這會在計(jì)數(shù)時產(chǎn)生較少的對象。
- 讓我們再次運(yùn)行以下代碼塊:
contours, hierarchy= cv2.findContours(canny, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
print("Number of objects found = ", len(contours))
這將產(chǎn)生以下輸出:
Number of objects found = 9
這是期望值。 但是,只有在特殊情況下,該值才是準(zhǔn)確的。
- 最后,讓我們嘗試概述檢測到的對象。 為此,我們繪制了
findContours()
方法的上一步中確定的輪廓:
_ = cv2.drawContours(image, contours, -1, (0,255,0), 10)
showim(image)
這將產(chǎn)生以下輸出:
請注意,我們已經(jīng)在拍攝的原始圖像中非常準(zhǔn)確地識別出了九片水果。 我們可以進(jìn)一步擴(kuò)展此示例,以在任何圖像中找到某些類型的對象。
要了解有關(guān) OpenCV 的更多信息并找到一些可供學(xué)習(xí)的示例,請?jiān)L問以下存儲庫。
現(xiàn)在讓我們學(xué)習(xí)如何處理音頻文件。
音頻處理
我們已經(jīng)看到了如何處理圖像以及可以從中提取信息。 在本節(jié)中,我們將介紹音頻文件的處理。 音頻或聲音是吞沒您周圍環(huán)境的東西。 在許多情況下,您僅能從該區(qū)域的音頻剪輯中正確預(yù)測該區(qū)域或環(huán)境,而無需實(shí)際看到任何視覺提示。 聲音或語音是人與人之間交流的一種形式。 安排良好的節(jié)奏模式形式的音頻稱為音樂,可以使用樂器制作。
音頻文件的一些流行格式如下:
- MP3:一種非常流行的格式,廣泛用于共享音樂文件。
- AAC:是對 MP3 格式的改進(jìn),AAC 主要用于 Apple 設(shè)備。
- WAV:由 Microsoft 和 IBM 創(chuàng)建,這種格式是無損壓縮,即使對于小的音頻文件也可能很大。
- MIDI:樂器數(shù)字接口文件實(shí)際上不包含音頻。 它們包含樂器音符,因此體積小且易于使用。
音頻處理是以下技術(shù)的增長所必需的:
- 用于基于語音的界面或助手的語音處理
- 虛擬助手的語音生成
- 音樂生成
- 字幕生成
- 推薦類似音樂
TensorFlow 團(tuán)隊(duì)的 Magenta 是一種非常流行的音頻處理工具。
您可以通過這里訪問 Magenta 主頁。 該工具允許快速生成音頻和音頻文件的轉(zhuǎn)錄。
讓我們簡要地探討 Magenta。
Magenta
Magenta 是 Google Brain 團(tuán)隊(duì)參與研究的一部分,該團(tuán)隊(duì)也參與了 TensorFlow。 它被開發(fā)為一種工具,可允許藝術(shù)家借助深度學(xué)習(xí)和強(qiáng)化學(xué)習(xí)算法來增強(qiáng)其音樂或藝術(shù)創(chuàng)作渠道。 這是 Magenta 的徽標(biāo):
讓我們從以下步驟開始:
- 要在系統(tǒng)上安裝 Magenta,可以使用 Python 的 pip 存儲庫:
pip install magenta
- 如果缺少任何依賴項(xiàng),則可以使用以下命令安裝它們:
!apt-get update -qq && apt-get install -qq libfluidsynth1 fluid-soundfont-gm build-essential libasound2-dev libjack-dev!pip install -qU pyfluidsynth pretty_midi
- 要將 Magenta 導(dǎo)入項(xiàng)目中,可以使用以下命令:
import magenta
或者,按照流行的慣例,僅加載 Magenta 的音樂部分,可以使用以下命令:
import magenta.music as mm
您可以使用前面的導(dǎo)入在線找到很多樣本。
讓我們快速創(chuàng)作一些音樂。 我們將創(chuàng)建一些鼓聲,然后將其保存到 MIDI 文件:
- 我們首先需要創(chuàng)建一個
NoteSequence
對象。 在 Magenta 中,所有音樂都以音符序列的格式存儲,類似于 MIDI 存儲音樂的方式:
from magenta.protobuf import music_pb2drums = music_pb2.NoteSequence()
- 創(chuàng)建
NoteSequence
對象后,該對象為空,因此我們需要向其添加一些注解:
drums.notes.add(pitch=36, start_time=0, end_time=0.125, is_drum=True, instrument=10, velocity=80)
drums.notes.add(pitch=38, start_time=0, end_time=0.125, is_drum=True, instrument=10, velocity=80)
drums.notes.add(pitch=42, start_time=0, end_time=0.125, is_drum=True, instrument=10, velocity=80)
drums.notes.add(pitch=46, start_time=0, end_time=0.125, is_drum=True, instrument=10, velocity=80)
.
.
.
drums.notes.add(pitch=42, start_time=0.75, end_time=0.875, is_drum=True, instrument=10, velocity=80)
drums.notes.add(pitch=45, start_time=0.75, end_time=0.875, is_drum=True, instrument=10, velocity=80)
請注意,在前面的代碼中,每個音符都有音高和力度。 再次類似于 MIDI 文件。
- 現(xiàn)在讓我們?yōu)橐舴砑庸?jié)奏,并設(shè)置音樂播放的總時間:
drums.total_time = 1.375drums.tempos.add(qpm=60)
完成此操作后,我們現(xiàn)在準(zhǔn)備導(dǎo)出 MIDI 文件。
- 我們首先需要將 Magenta
NoteSequence
對象轉(zhuǎn)換為 MIDI 文件:
mm.sequence_proto_to_midi_file(drums, 'drums_sample_output.mid')
前面的代碼首先將音符序列轉(zhuǎn)換為 MIDI,然后將它們寫入磁盤上的drums_sample_output.mid
文件。 您現(xiàn)在可以使用任何合適的音樂播放器播放midi
文件。
繼續(xù)前進(jìn),讓我們探索如何處理視頻。
視頻處理
視頻處理是多媒體處理的另一個重要部分。 通常,我們需要弄清楚移動場景中發(fā)生的事情。 例如,如果我們要生產(chǎn)自動駕駛汽車,則它需要實(shí)時處理大量視頻才能平穩(wěn)行駛。 這種情況的另一個實(shí)例可以是將手語轉(zhuǎn)換為文本以幫助與語音障礙者互動的設(shè)備。 此外,需要視頻處理來創(chuàng)建電影和動作效果。
我們將在本節(jié)中再次探討 OpenCV。 但是,我們將演示如何在 OpenCV 中使用實(shí)時攝像機(jī)供稿來檢測面部。
創(chuàng)建一個新的 Python 腳本并執(zhí)行以下步驟:
- 首先,我們需要對腳本進(jìn)行必要的導(dǎo)入。 這將很簡單,因?yàn)槲覀冎恍枰?OpenCV 模塊:
import cv2
- 現(xiàn)在,讓我們將 Haar 級聯(lián)模型加載到腳本中。 Haar 級聯(lián)算法是一種用于檢測任何給定圖像中的對象的算法。 由于視頻不過是圖像流,因此我們將其分解為一系列幀并檢測其中的人臉:
faceCascade = cv2.CascadeClassifier("haarcascade_frontalface_default.xml")
您將不得不從以下位置獲取haarcascade_frontalface_default.xml
文件。
Haar 級聯(lián)是一類使用級聯(lián)函數(shù)執(zhí)行分類的分類器算法。 保羅·維奧拉(Paul Viola)和邁克爾·瓊斯(Michael Jones)引入了它們,以試圖建立一種對象檢測算法,該算法足夠快以在低端設(shè)備上運(yùn)行。 級聯(lián)函數(shù)池來自幾個較小的分類器。
Haar 級聯(lián)文件通常以可擴(kuò)展標(biāo)記語言(XML)的格式找到,并且通常執(zhí)行一項(xiàng)特定功能,例如面部檢測,身體姿勢檢測, 對象檢測等。 您可以在此處閱讀有關(guān) Haar 級聯(lián)的更多信息。
- 現(xiàn)在,我們必須實(shí)例化攝像機(jī)以進(jìn)行視頻捕獲。 為此,我們可以使用默認(rèn)的筆記本電腦攝像頭:
video_capture = cv2.VideoCapture(0)
- 現(xiàn)在讓我們從視頻中捕獲幀并顯示它們:
while True:# Capture framesret, frame = video_capture.read()### We'll add code below in future steps### We'll add code above in future steps# Display the resulting framecv2.imshow('Webcam Capture', frame)if cv2.waitKey(1) & 0xFF == ord('q'):break
這樣您就可以在屏幕上顯示實(shí)時視頻供稿。 在運(yùn)行此程序之前,我們需要釋放相機(jī)并正確關(guān)閉窗戶。
- 要正確關(guān)閉實(shí)時捕獲,請使用以下命令:
video_capture.release()
cv2.destroyAllWindows()
現(xiàn)在,讓我們對腳本進(jìn)行測試運(yùn)行。
您應(yīng)該會看到一個窗口,其中包含您的臉部實(shí)時捕捉的圖像(如果您不害羞的話)。
- 讓我們向該視頻提要添加面部檢測。 由于用于面部檢測的 Haar 級聯(lián)在使用灰度圖像時效果更好,因此我們將首先將每個幀轉(zhuǎn)換為灰度,然后對其進(jìn)行面部檢測。 我們需要將此代碼添加到
while
循環(huán)中,如以下代碼所示:
### We'll add code below in future stepsgray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)faces = faceCascade.detectMultiScale(gray,scaleFactor=1.1,minNeighbors=5,minSize=(30, 30),flags=cv2.CASCADE_SCALE_IMAGE)### We'll add code above in future steps
這樣,我們就可以檢測到人臉了,因此讓我們在視頻供稿中對其進(jìn)行標(biāo)記!
- 我們將簡單地使用 OpenCV 的矩形繪制函數(shù)在屏幕上標(biāo)記面孔:
minNeighbors=5,minSize=(30, 30),flags=cv2.CASCADE_SCALE_IMAGE)for (x, y, w, h) in faces:cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2)### We'll add code above in future steps
現(xiàn)在讓我們再次嘗試運(yùn)行腳本。
轉(zhuǎn)到終端并使用以下命令運(yùn)行腳本:
python filename.py
在這里,文件名是您保存腳本文件時的名稱。
您應(yīng)該獲得類似于以下屏幕截圖的輸出:
要退出實(shí)時網(wǎng)絡(luò)攝像頭捕獲,請使用鍵盤上的Q
鍵(我們已在前面的代碼中進(jìn)行了設(shè)置)。
我們已經(jīng)研究了多媒體處理的三種主要形式的概述。 現(xiàn)在,讓我們繼續(xù)前進(jìn),構(gòu)建基于 LSTM 的模型以生成音頻。
開發(fā)基于 RNN 的音樂生成模型
在本節(jié)中,我們將開發(fā)音樂生成模型。 我們將為此使用 RNN,并使用 LSTM 神經(jīng)元模型。 RNN 與簡單的人工神經(jīng)網(wǎng)絡(luò)(ANN)有很大的不同-允許在層之間重復(fù)使用輸入。
雖然在 ANN 中,我們希望輸入到神經(jīng)網(wǎng)絡(luò)的輸入值向前移動,然后產(chǎn)生基于錯誤的反饋,并將其合并到網(wǎng)絡(luò)權(quán)重中,但 RNN 使輸入多次循環(huán)返回到先前的層。
下圖表示 RNN 神經(jīng)元:
從上圖可以看到,通過神經(jīng)元激活函數(shù)后的輸入分為兩部分。 一部分在網(wǎng)絡(luò)中向前移動到下一層或輸出,而另一部分則反饋到網(wǎng)絡(luò)中。 在時間序列數(shù)據(jù)集中,可以相對于給定樣本在t
的時間標(biāo)記每個樣本,我們可以擴(kuò)展前面的圖,如下所示:
但是,由于通過激活函數(shù)反復(fù)暴露值,RNN 趨向于梯度消失,其中 RNN 的值逐梯度小到可以忽略不計(jì)(或在梯度爆炸的情況下變大)。 為避免這種情況,引入了 LSTM 單元,該單元通過將信息存儲在單元中而允許將信息保留更長的時間。 每個 LSTM 單元由三個門和一個存儲單元組成。 三個門(輸入,輸出和遺忘門)負(fù)責(zé)確定哪些值存儲在存儲單元中。
因此,LSTM 單元變得獨(dú)立于 RNN 其余部分的更新頻率,并且每個單元格都有自己的時間來記住它所擁有的值。 就我們而言,與其他信息相比,我們忘記了一些隨機(jī)信息的時間要晚得多,這更自然地模仿了自然。
您可以在以下鏈接中找到有關(guān) RNN 和 LSTM 的詳細(xì)且易于理解的解釋。
在開始為項(xiàng)目構(gòu)建模型之前,我們需要設(shè)置項(xiàng)目目錄,如以下代碼所示:
├── app.py
├── MusicGenerate.ipynb
├── Output/
└── Samples/├── 0.mid├── 1.mid├── 2.mid└── 3.mid
請注意,我們已經(jīng)在Samples
文件夾中下載了四個 MIDI 文件樣本。 然后,我們創(chuàng)建了要使用的MusicGenerate.ipynb
Jupyter 筆記本。 在接下來的幾個步驟中,我們將僅在此 Jupyter 筆記本上工作。 app.py
腳本當(dāng)前為空,將來,我們將使用它來托管模型。
現(xiàn)在讓我們開始創(chuàng)建基于 LSTM 的用于生成音樂的模型。
創(chuàng)建基于 LSTM 的模型
在本節(jié)中,我們將在 Jupyter 筆記本環(huán)境中研究MusicGenerate.ipynb
筆記本:
- 在此筆記本中,我們將需要導(dǎo)入許多模塊。 使用以下代碼導(dǎo)入它們:
import mido
from mido import MidiFile, MidiTrack, Message
from tensorflow.keras.layers import LSTM, Dense, Activation, Dropout, Flatten
from tensorflow.keras.preprocessing import sequence
from tensorflow.keras.models import Sequential
from tensorflow.keras.optimizers import Adam
from sklearn.preprocessing import MinMaxScaler
import numpy as np
我們使用了mido
庫。 如果您的系統(tǒng)上未安裝它,則可以使用以下命令來安裝它:
pip install mido
注意,在前面的代碼中,我們還導(dǎo)入了 Keras 模塊和子部件。 該項(xiàng)目中使用的 TensorFlow 版本為 2.0。 為了在您的系統(tǒng)上安裝相同版本或升級當(dāng)前的 TensorFlow 安裝,可以使用以下命令:
pip install --upgrade pippip install --upgrade tensorflow
現(xiàn)在,我們將繼續(xù)閱讀示例文件。
- 要將 MIDI 文件讀入項(xiàng)目筆記本,請使用以下代碼:
notes = []
for msg in MidiFile('Samples/0.mid') :try:if not msg.is_meta and msg.channel in [0, 1, 2, 3] and msg.type == 'note_on':data = msg.bytes()notes.append(data[1])except:pass
這將在notes
列表中加載通道0
,1
,2
和3
的所有開頭音符。
要了解有關(guān)注解,消息和頻道的更多信息,請使用以下文檔。
- 由于音符處于大于 0–1 范圍的可變范圍內(nèi),因此我們將使用以下代碼將其縮放以適合公共范圍:
scaler = MinMaxScaler(feature_range=(0,1))
scaler.fit(np.array(notes).reshape(-1,1))
notes = list(scaler.transform(np.array(notes).reshape(-1,1)))
- 我們基本上擁有的是隨時間變化的筆記列表。 我們需要將其轉(zhuǎn)換為時間序列數(shù)據(jù)集格式。 為此,我們使用以下代碼轉(zhuǎn)換列表:
notes = [list(note) for note in notes]X = []
y = []n_prev = 20
for i in range(len(notes)-n_prev):X.append(notes[i:i+n_prev])y.append(notes[i+n_prev])
我們已將其轉(zhuǎn)換為一個集合,其中每個樣本都帶有未來的 20 個音符,并且在數(shù)據(jù)集的末尾具有過去的 20 個音符。這可以通過以下方式進(jìn)行:如果我們有 5 個樣本,例如M[1]
,M[2]
,M[3]
,M[4]
和M[5]
,然后我們將它們安排在大小為 2 的配對中(類似于我們的 20),如下所示:
M[1] M[2]
M[2] M[3]
M[3] M[4]
,依此類推
- 現(xiàn)在,我們將使用 Keras 創(chuàng)建 LSTM 模型,如以下代碼所示:
model = Sequential()
model.add(LSTM(256, input_shape=(n_prev, 1), return_sequences=True))
model.add(Dropout(0.3))
model.add(LSTM(128, input_shape=(n_prev, 1), return_sequences=True))
model.add(Dropout(0.3))
model.add(LSTM(256, input_shape=(n_prev, 1), return_sequences=False))
model.add(Dropout(0.3))
model.add(Dense(1))
model.add(Activation('linear'))
optimizer = Adam(lr=0.001)
model.compile(loss='mse', optimizer=optimizer)
隨意使用此 LSTM 模型的超參數(shù)。
- 最后,我們將訓(xùn)練樣本適合模型并保存模型文件:
model.fit(np.array(X), np.array(y), 32, 25, verbose=1)
model.save("model.h5")
這將在我們的項(xiàng)目目錄中創(chuàng)建model.h5
文件。 每當(dāng)用戶從應(yīng)用發(fā)出生成請求時,我們都會將此文件與其他音樂樣本一起使用,以隨機(jī)生成新的樂曲。
現(xiàn)在,讓我們使用 Flask 服務(wù)器部署此模型。
使用 Flask 部署模型
對于項(xiàng)目的這一部分,您可以使用本地系統(tǒng),也可以在其他地方的app.py
中部署腳本。 我們將編輯此文件以創(chuàng)建 Flask 服務(wù)器,該服務(wù)器生成音樂并允許下載生成的 MIDI 文件。
該文件中的某些代碼與 Jupyter 筆記本類似,因?yàn)槊看渭虞d音頻樣本并將其與我們生成的模型一起使用時,音頻樣本始終需要進(jìn)行類似的處理:
- 我們使用以下代碼將所需的模塊導(dǎo)入此腳本:
import mido
from mido import MidiFile, MidiTrack, Message
from tensorflow.keras.models import load_model
from sklearn.preprocessing import MinMaxScaler
import numpy as np
import random
import time
from flask import send_file
import osfrom flask import Flask, jsonifyapp = Flask(__name__)
請注意,我們進(jìn)行的最后四次導(dǎo)入與之前在 Jupyter 筆記本中導(dǎo)入的內(nèi)容不同。 同樣,我們不需要將幾個 Keras 組件導(dǎo)入此腳本,因?yàn)槲覀儗囊呀?jīng)準(zhǔn)備好的模型中加載。
在上一個代碼塊的最后一行代碼中,我們實(shí)例化了一個名為app
的 Flask 對象。
- 在此步驟中,我們將創(chuàng)建函數(shù)的第一部分,當(dāng)在 API 上調(diào)用
/generate
路由時,該函數(shù)將生成新的音樂樣本:
@app.route('/generate', methods=['GET'])
def generate():songnum = random.randint(0, 3)### More code below this
- 一旦我們隨機(jī)決定在音樂生成過程中使用哪個樣本文件,我們就需要像 Jupyter 筆記本中的訓(xùn)練樣本那樣對它進(jìn)行類似的轉(zhuǎn)換:
def generate():... notes = []for msg in MidiFile('Samples/%s.mid' % (songnum)):try:if not msg.is_meta and msg.channel in [0, 1, 2, 3] and msg.type == 'note_on':data = msg.bytes()notes.append(data[1])except:passscaler = MinMaxScaler(feature_range=(0, 1))scaler.fit(np.array(notes).reshape(-1, 1))notes = list(scaler.transform(np.array(notes).reshape(-1, 1)))### More code below this
在前面的代碼塊中,我們加載了示例文件,并從訓(xùn)練過程中使用的相同通道中提取了其注解。
- 現(xiàn)在,我們將像在訓(xùn)練期間一樣縮放音符:
def generate():... notes = [list(note) for note in notes]X = []y = []n_prev = 20for i in range(len(notes) - n_prev):X.append(notes[i:i + n_prev])y.append(notes[i + n_prev])### More code below this
我們也將這些筆記列表轉(zhuǎn)換為適合模型輸入的形狀,就像我們在訓(xùn)練過程中對輸入所做的一樣。
- 接下來,我們將使用以下代碼來加載 Keras 模型并從該模型創(chuàng)建新的注解列表:
def generate():... model = load_model("model.h5")xlen = len(X)start = random.randint(0, 100)stop = start + 200prediction = model.predict(np.array(X[start:stop]))prediction = np.squeeze(prediction)prediction = np.squeeze(scaler.inverse_transform(prediction.reshape(-1, 1)))prediction = [int(i) for i in prediction] ### More code below this
- 現(xiàn)在,我們可以使用以下代碼將此音符列表轉(zhuǎn)換為 MIDI 序列:
def generate():... mid = MidiFile()track = MidiTrack()t = 0for note in prediction:vol = random.randint(50, 70)note = np.asarray([147, note, vol])bytes = note.astype(int)msg = Message.from_bytes(bytes[0:3])t += 1msg.time = ttrack.append(msg)mid.tracks.append(track)### More code below this
- 現(xiàn)在,我們準(zhǔn)備將文件保存到磁盤。 它包含從模型隨機(jī)生成的音樂:
def generate():... epoch_time = int(time.time())outputfile = 'output_%s.mid' % (epoch_time)mid.save("Output/" + outputfile)response = {'result': outputfile}return jsonify(response)
因此,/generate
API 以 JSON 格式返回生成的文件的名稱。 然后,我們可以下載并播放此文件。
- 要將文件下載到客戶端,我們需要使用以下代碼:
@app.route('/download/<fname>', methods=['GET'])
def download(fname):return send_file("Output/"+fname, mimetype="audio/midi", as_attachment=True)
請注意,前面的函數(shù)在/download/filename
路由上起作用,在該路由上,客戶端根據(jù)上一代 API 調(diào)用的輸出提供文件名。 下載的文件的 MIME 類型為audio/midi
,它告訴客戶端它是 MIDI 文件。
- 最后,我們可以添加將執(zhí)行此服務(wù)器的代碼:
if __name__ == '__main__':app.run(host="0.0.0.0", port=8000)
完成此操作后,我們可以在終端中使用以下命令來運(yùn)行服務(wù)器:
python app.py
如果代碼中產(chǎn)生任何警告,您將從控制臺獲得一些調(diào)試信息。 完成此操作后,我們準(zhǔn)備在下一節(jié)中為我們的 API 構(gòu)建 Flutter 應(yīng)用客戶端。
在 Android 和 iOS 上部署音頻生成 API
成功創(chuàng)建和部署模型后,現(xiàn)在開始構(gòu)建移動應(yīng)用。 該應(yīng)用將用于獲取和播放由先前創(chuàng)建的模型生成的音樂。
它將具有三個按鈕:
- 生成音樂:生成新的音頻文件
- 播放:播放新生成的文件
- 停止:停止正在播放的音樂
另外,它的底部將顯示一些文本,以顯示應(yīng)用的當(dāng)前狀態(tài)。
該應(yīng)用將顯示如下:
該應(yīng)用的小部件樹如下所示:
現(xiàn)在開始構(gòu)建應(yīng)用的 UI。
創(chuàng)建 UI
我們首先創(chuàng)建一個新的 Dart 文件play_music.dart
和一個有狀態(tài)的小部件PlayMusic
。 如前所述,在該文件中,我們將創(chuàng)建三個按鈕來執(zhí)行基本功能。 以下步驟描述了如何創(chuàng)建 UI:
- 定義
buildGenerateButton()
方法以創(chuàng)建RaisedButton
變量,該變量將用于生成新的音樂文件:
Widget buildGenerateButton() {return Padding(padding: EdgeInsets.only(left: 16, right: 16, top: 16),child: RaisedButton(child: Text("Generate Music"),color: Colors.blue,textColor: Colors.white,),);}
在前面定義的函數(shù)中,我們創(chuàng)建一個RaisedButton
,并添加Generate Music
文本作為子元素。 color
屬性的Colors.blue
值用于為按鈕賦予藍(lán)色。 另外,我們將textColor
修改為Colors.white
,以使按鈕內(nèi)的文本為白色。 使用EdgeInsets.only()
給按鈕提供左,右和頂部填充。 在后面的部分中,我們將在按鈕上添加onPressed
屬性,以便每次按下按鈕時都可以從托管模型中獲取新的音樂文件。
- 定義
buildPlayButton()
方法以播放新生成的音頻文件:
Widget buildPlayButton() {return Padding(padding: EdgeInsets.only(left: 16, right: 16, top: 16),child: RaisedButton(child: Text("Play"),onPressed: () {play();},color: Colors.blue,textColor: Colors.white,),);}
在前面定義的函數(shù)中,我們創(chuàng)建一個RaisedButton
,并添加"Play"
文本作為子元素。 color
屬性的Colors.blue
值用于為按鈕賦予藍(lán)色。 另外,我們將textColor
修改為Colors.white
,以使按鈕內(nèi)的文本為白色。 使用EdgeInsets.only()
給按鈕提供左,右和頂部填充。 在后面的部分中,我們將在按鈕上添加onPressed
屬性,以在每次按下按鈕時播放新生成的音樂文件。
- 定義
buildStopButton()
方法以停止當(dāng)前正在播放的音頻:
Widget buildStopButton() {return Padding(padding: EdgeInsets.only(left: 16, right: 16, top: 16),child: RaisedButton(child: Text("Stop"),onPressed: (){stop();},color: Colors.blue,textColor: Colors.white,));}
在前面定義的函數(shù)中,我們創(chuàng)建一個RaisedButton
,并添加"Stop"
文本作為子元素。 color
屬性的Colors.blue
值用于為按鈕賦予藍(lán)色。 另外,我們將textColor
修改為Colors.white
,以使按鈕內(nèi)的文本為白色。 使用EdgeInsets.only()
給按鈕提供左,右和頂部填充。 在下一節(jié)中,我們將向按鈕添加onPressed
屬性,以在按下按鈕時停止當(dāng)前播放的音頻。
- 覆蓋
PlayMusicState
中的build()
方法,以創(chuàng)建先前創(chuàng)建的按鈕的Column
:
@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text("Generate Play Music"),),body: Column(crossAxisAlignment: CrossAxisAlignment.stretch,children: <Widget>[buildGenerateButton(),buildPlayButton(),buildStopButton(),],));}
在前面的代碼片段中,我們返回Scaffold
。 它包含一個AppBar
,其中具有[Generate Play Music]作為title
。 Scaffold
的主體是Column
。 列的子級是我們在上一步中創(chuàng)建的按鈕。 通過調(diào)用相應(yīng)方法將按鈕添加到該列中。 此外,crossAxisAlignment
屬性設(shè)置為CrossAxisAlignment.stretch
,以便按鈕占據(jù)父容器(即列)的總寬度。
此時,該應(yīng)用如下所示:
在下一節(jié)中,我們將添加一種在應(yīng)用中播放音頻文件的機(jī)制。
添加音頻播放器
創(chuàng)建應(yīng)用的用戶界面后,我們現(xiàn)在將音頻播放器添加到應(yīng)用中以播放音頻文件。 我們將使用audioplayer
插件添加音頻播放器,如下所示:
- 我們首先將依賴項(xiàng)添加到
pubspec.yaml
文件中:
audioplayers: 0.13.2
現(xiàn)在,通過運(yùn)行flutter pub get
獲得包。
- 接下來,我們將插件導(dǎo)入
play_music.dart
。
import 'package:audioplayers/audioplayers.dart';
- 然后,在
PlayMusicState
內(nèi)創(chuàng)建AudioPlayer
的實(shí)例:
AudioPlayer audioPlayer = AudioPlayer();
- 現(xiàn)在,讓我們定義一個
play()
方法來播放遠(yuǎn)程可用的音頻文件,如下所示:
play() async {var url = 'http://34.70.80.18:8000/download/output_1573917221.mid';int result = await audioPlayer.play(url);if (result == 1) {print('Success');}}
最初,我們將使用存儲在url
變量中的樣本音頻文件。 通過傳遞url
中的值,使用audioPlayer.play()
播放音頻文件。 另外,如果從url
變量成功訪問和播放了音頻文件,則結(jié)果將存儲在結(jié)果變量中,其值將為1
。
- 現(xiàn)在,將
onPressed
屬性添加到buildPlayButton
內(nèi)置的播放按鈕中,以便每當(dāng)按下該按鈕時就播放音頻文件:
Widget buildPlayButton() {return Padding(padding: EdgeInsets.only(left: 16, right: 16, top: 16),child: RaisedButton(....onPressed: () {play();},....),);}
在前面的代碼片段中,我們添加onPressed
屬性并調(diào)用play()
方法,以便每當(dāng)按下按鈕時就播放音頻文件。
- 現(xiàn)在,我們將定義
stop()
以停止正在播放的音樂:
void stop() {audioPlayer.stop();}
在stop()
方法內(nèi)部,我們只需調(diào)用audioPlayer.stop()
即可停止正在播放的音樂。
- 最后,我們?yōu)?code>buildStopButton()中內(nèi)置的停止按鈕添加
onPressed
屬性:
Widget buildStopButton() {return Padding(padding: EdgeInsets.only(left: 16, right: 16, top: 16),child: RaisedButton(....onPressed: (){stop();},....));}
在前面的代碼片段中,我們向onPressed
中的stop()
添加了一個調(diào)用,以便一旦按下停止按鈕就停止音頻。
現(xiàn)在開始使用 Flutter 應(yīng)用部署模型。
部署模型
在為應(yīng)用成功添加基本的播放和停止功能之后,現(xiàn)在讓我們訪問托管模型以每次生成,獲取和播放新的音頻文件。 以下步驟詳細(xì)討論了如何在應(yīng)用內(nèi)部訪問模型:
- 首先,我們定義
fetchResponse()
方法來生成和獲取新的音頻文件:
void fetchResponse() async {final response =await http.get('http://35.225.134.65:8000/generate');if (response.statusCode == 200) {var v = json.decode(response.body);fileName = v["result"] ;} else {throw Exception('Failed to load');}}
我們首先使用http.get()
從 API 獲取響應(yīng),然后傳入托管模型的 URL。 get()
方法的響應(yīng)存儲在response
變量中。 get()
操作完成后,我們使用response.statusCode
檢查狀態(tài)碼。 如果狀態(tài)值為200
,則獲取成功。 接下來,我們使用json.decode()
將響應(yīng)的主體從原始 JSON 轉(zhuǎn)換為Map<String,dynamic>
,以便可以輕松訪問響應(yīng)主體中包含的鍵值對。 我們使用v["result"]
訪問新音頻文件的值,并將其存儲在全局fileName
變量中。 如果responseCode
不是200
,我們只會拋出一個錯誤。
- 現(xiàn)在讓我們定義
load()
以對fetchResponse()
進(jìn)行適當(dāng)?shù)恼{(diào)用:
void load() {fetchResponse();}
在前面的代碼行中,我們僅定義一個load()
方法,該方法用于調(diào)用fetchResponse()
來獲取新生成的音頻文件的值。
- 現(xiàn)在,我們將修改
buildGenerateButton()
中的onPressed
屬性,以每次生成新的音頻文件:
Widget buildGenerateButton() {return Padding(....child: RaisedButton(....onPressed: () {load();},....),);}
根據(jù)應(yīng)用的功能,每當(dāng)按下生成按鈕時,都應(yīng)生成一個新的音頻文件。 這直接意味著無論何時按下“生成”按鈕,我們都需要調(diào)用 API 以獲取新生成的音頻文件的名稱。 因此,我們修改buildGenerateButton()
以添加onPressed
屬性,以便每當(dāng)按下按鈕時,它都會調(diào)用load()
,該調(diào)用隨后將調(diào)用fetchResponse()
并將新音頻文件的名稱存儲在輸出中。
- 托管的音頻文件有兩個部分,
baseUrl
和fileName
。baseUrl
對于所有調(diào)用均保持不變。 因此,我們聲明一個存儲baseUrl
的全局字符串變量:
String baseUrl = 'http://34.70.80.18:8000/download/';
回想一下,我們已經(jīng)在“步驟 1”中將新音頻文件的名稱存儲在fileName
中。
- 現(xiàn)在,讓我們修改
play()
以播放新生成的文件:
play() async {var url = baseUrl + fileName;AudioPlayer.logEnabled = true;int result = await audioPlayer.play(url);if (result == 1) {print('Success');}}
在前面的代碼片段中,我們修改了前面定義的play()
方法。 我們通過附加baseUrl
和fileName
創(chuàng)建一個新的 URL,以便url
中的值始終與新生成的音頻文件相對應(yīng)。 我們在調(diào)用audioPlayer.play()
時傳遞 URL 的值。 這樣可以確保每次按下播放按鈕時,都會播放最新生成的音頻文件。
- 此外,我們添加了
Text
小部件以反映文件生成狀態(tài):
Widget buildLoadingText() {return Center(child: Padding(padding: EdgeInsets.only(top: 16),child: Text(loadText)));}
在前面定義的函數(shù)中,我們創(chuàng)建了一個簡單的Text
小部件,以反映提取操作正在運(yùn)行以及何時完成的事實(shí)。 Text
小部件具有頂部填充,并與Center
對齊。 loadText
值用于創(chuàng)建窗口小部件。
全局聲明該變量,其初始值為'Generate Music'
:
String loadText = 'Generate Music';
- 更新
build()
方法以添加新的Text
小部件:
@overrideWidget build(BuildContext context) {return Scaffold(....body: Column(....children: <Widget>[buildGenerateButton(),....buildLoadingText()],));}
現(xiàn)在,我們更新build()
方法以添加新創(chuàng)建的Text
小部件。 該窗口小部件只是作為先前創(chuàng)建的Column
的子級添加的。
- 當(dāng)用戶想要生成一個新的文本文件時,并且在進(jìn)行提取操作時,我們需要更改文本:
void load() {setState(() {loadText = 'Generating...';});fetchResponse();}
在前面的代碼段中,loadText
值設(shè)置為'Generating...'
,以反映正在進(jìn)行get()
操作的事實(shí)。
- 最后,獲取完成后,我們將更新文本:
void fetchResponse() async {final response =await http.get('http://35.225.134.65:8000/generate').whenComplete((){setState(() {loadText = 'Generation Complete';});});....}
提取完成后,我們將loadText
的值更新為'Generation Complete'
。 這表示應(yīng)用現(xiàn)在可以播放新生成的文件了。
可以在此處查看play_music.dart
的整個代碼。
在使應(yīng)用的所有部分正常工作之后,現(xiàn)在讓我們通過創(chuàng)建最終的材質(zhì)應(yīng)用將所有內(nèi)容放在一起。
創(chuàng)建最終的材質(zhì)應(yīng)用
現(xiàn)在創(chuàng)建main.dart
文件。 該文件包含無狀態(tài)窗口小部件MyApp
。 我們重寫build()
方法并將PlayMusic
設(shè)置為其子級:
@overrideWidget build(BuildContext context) {return MaterialApp(title: 'Flutter Demo',theme: ThemeData(primarySwatch: Colors.blue,),home: PlayMusic(),);}
在覆蓋的build()
方法中,我們簡單地將home
創(chuàng)建為PlayMusic()
的MaterialApp
。
整個項(xiàng)目可以在這里查看。
總結(jié)
在本章中,我們通過將多媒體處理分解為圖像,音頻和視頻處理的核心組件來進(jìn)行研究,并討論了一些最常用的處理工具。 我們看到了使用 OpenCV 執(zhí)行圖像或視頻處理變得多么容易。 另外,我們看到了一個使用 Magenta 生成鼓音樂的簡單示例。 在本章的下半部分,我們介紹了 LSTM 如何與時間序列數(shù)據(jù)一起使用,并構(gòu)建了一個 API,該 API 可以從提供的樣本文件生成器樂。 最后,我們將此 API 與 Flutter 應(yīng)用結(jié)合使用,該應(yīng)用是跨平臺的,可以同時部署在 Android,iOS 和 Web 上。
在下一章中,我們將研究如何使用深度強(qiáng)化學(xué)習(xí)(DRL)來創(chuàng)建可以玩棋盤游戲(例如國際象棋)的智能體。
八、基于強(qiáng)化神經(jīng)網(wǎng)絡(luò)的國際象棋引擎
在幾個在線應(yīng)用商店以及幾乎每個軟件商店中,游戲都提供了自己的完整版塊。 游戲的重要性和熱情不容忽視,這就是為什么全世界的開發(fā)人員都在不斷嘗試開發(fā)出更好,更吸引人的游戲的原因。
在流行的棋盤游戲世界中,國際象棋是全世界最有競爭力和最復(fù)雜的游戲之一。 已經(jīng)嘗試了一些強(qiáng)大的自動化程序來下棋和與人類競爭。 本章將討論 DeepMind 的開發(fā)人員所使用的方法,他們創(chuàng)建了 Alpha Zero,這是一種自學(xué)算法,可以自學(xué)下棋,從而能夠以一個單打擊敗市場上當(dāng)時最好的國際象棋 AI,Stockfish 8。 在短短 24 小時的訓(xùn)練中得分較高。
在本章中,我們將介紹您需要理解的概念,以便構(gòu)建這種深度強(qiáng)化學(xué)習(xí)算法,然后構(gòu)建示例項(xiàng)目。 請注意,該項(xiàng)目將要求您具有 Python 和機(jī)器學(xué)習(xí)的豐富知識。
我們將在本章介紹以下主題:
- 強(qiáng)化學(xué)習(xí)導(dǎo)論
- 手機(jī)游戲中的強(qiáng)化學(xué)習(xí)
- 探索 Google 的 DeepMind
- 適用于 Connect 4 的 Alpha 類零 AI
- 基礎(chǔ)項(xiàng)目架構(gòu)
- 為國際象棋引擎開發(fā) GCP 托管的 REST API
- 在 Android 上創(chuàng)建簡單的國際象棋 UI
- 將國際象棋引擎 API 與 UI 集成
讓我們從討論增強(qiáng)學(xué)習(xí)智能體在手機(jī)游戲中的用法和普及程度開始。
強(qiáng)化學(xué)習(xí)導(dǎo)論
在過去的幾年中,強(qiáng)化學(xué)習(xí)已成為機(jī)器學(xué)習(xí)研究人員中一個重要的研究領(lǐng)域。 人們越來越多地使用它來構(gòu)建能夠在任何給定環(huán)境中表現(xiàn)更好的智能體,以尋求對他們所執(zhí)行行為的更好回報。 簡而言之,這為我們提供了強(qiáng)化學(xué)習(xí)的定義–在人工智能領(lǐng)域,這是一種算法,旨在創(chuàng)建虛擬的智能體,它可在任何給定條件下,在環(huán)境中執(zhí)行動作,在執(zhí)行一系列動作后,取得最佳的獎勵。
讓我們嘗試通過定義與通用強(qiáng)化學(xué)習(xí)算法關(guān)聯(lián)的變量來賦予此定義更多的結(jié)構(gòu):
- 智能體:執(zhí)行動作的虛擬實(shí)體。 是替換游戲/軟件的指定用戶的實(shí)體。
- 操作(
a
):智能體可以執(zhí)行的可能操作。 - 環(huán)境(
e
):在軟件/游戲中可用的一組場景。 - 狀態(tài)(
S
):所有方案的集合,以及其中可用的配置。 - 獎勵(
R
):對于智能體執(zhí)行的任何操作返回的值,然后智能體嘗試將其最大化。 - 策略(
π
):智能體用來確定接下來必須執(zhí)行哪些操作的策略。 - 值(
V
):R
是短期每動作獎勵,而值是在一組動作結(jié)束時預(yù)期的總獎勵。V[π](s)
通過遵循狀態(tài)S
下的策略π
來定義預(yù)期的總回報。
下圖顯示了該算法的流程:
盡管我們在前面的定義列表中沒有提到觀察者,但必須有觀察者或評估者才能產(chǎn)生獎勵。 有時,觀察者本身可能是一個復(fù)雜的軟件,但是通常,這是一個簡單的評估函數(shù)或指標(biāo)。
要獲得關(guān)于強(qiáng)化學(xué)習(xí)的更詳細(xì)的想法,您可以閱讀這個頁面上的 Wikipedia 文章。 有關(guān)正在使用的強(qiáng)化學(xué)習(xí)智能體的快速樣本,請閱讀以下 DataCamp 文章。
在下一部分中,我們將學(xué)習(xí)強(qiáng)化學(xué)習(xí)在手機(jī)游戲中的地位。
手機(jī)游戲中的強(qiáng)化學(xué)習(xí)
出于各種原因而希望構(gòu)建具有游戲性的 AI 的開發(fā)人員中,強(qiáng)化學(xué)習(xí)已變得越來越流行-只需檢查 AI 的功能,建立可以幫助專業(yè)人士改善游戲水平的訓(xùn)練智能體等等。 從研究人員的角度來看,游戲?yàn)閺?qiáng)化學(xué)習(xí)智能體提供了最佳的測試環(huán)境,可以根據(jù)經(jīng)驗(yàn)做出決策并學(xué)習(xí)在任何給定環(huán)境中的生存/成就。 這是因?yàn)榭梢允褂煤唵味_的規(guī)則設(shè)計(jì)游戲,從而可以準(zhǔn)確預(yù)測環(huán)境對特定動作的反應(yīng)。 這使得更容易評估強(qiáng)化學(xué)習(xí)智能體的表現(xiàn),從而為 AI 提供良好的訓(xùn)練基礎(chǔ)。 考慮到在玩游戲的 AI 方面的突破,也有人表示,我們向通用 AI 的發(fā)展速度比預(yù)期的要快。 但是強(qiáng)化學(xué)習(xí)概念如何映射到游戲?
讓我們考慮一個簡單的游戲,例如井字棋。 另外,如果您覺得古怪,只需使用 Google 搜索井字棋,您就會在搜索結(jié)果中看到一個游戲!
考慮您正在用計(jì)算機(jī)玩井字棋。 這里的計(jì)算機(jī)是智能體。 在這種情況下,環(huán)境是什么? 您猜對了–井字棋板以及在環(huán)境中管理游戲的一組規(guī)則。 井字棋盤上已經(jīng)放置的標(biāo)記可以確定環(huán)境所在的狀態(tài)。座席可以在棋盤上放置的X
或O
是他們可以執(zhí)行的動作,即輸?shù)?#xff0c;贏得比賽或平局。 或朝著損失,勝利或平局前進(jìn)是他們執(zhí)行任何行動后回饋給智能體的獎勵。 智能體贏得比賽所遵循的策略是遵循的策略。
因此,從該示例可以得出結(jié)論,強(qiáng)化學(xué)習(xí)智能體非常適合構(gòu)建學(xué)習(xí)玩任何游戲的 AI。 這導(dǎo)致許多開發(fā)人員想出了象圍棋,跳棋,反恐精英等國際象棋以外的幾種流行游戲的游戲 AI。 甚至 Chrome Dino 之類的游戲也發(fā)現(xiàn)開發(fā)人員試圖使用 AI 進(jìn)行游戲。
在下一部分中,我們將簡要概述 Google 的 DeepMind,它是游戲 AI 制造商領(lǐng)域中最受歡迎的公司之一。
探索 Google 的 DeepMind
當(dāng)您談?wù)撟詫W(xué)習(xí)人工智能的發(fā)展時,DeepMind 可能是最著名的名稱之一,這是由于它們在該領(lǐng)域的開創(chuàng)性研究和成就。 自 2015 年 Google 重組以來,DeepMind 在 2014 年被 Google 收購,目前是 Alphabet 的全資子公司。DeepMind 最著名的作品包括 AlphaGo 及其繼任者 Alpha Zero。 讓我們更深入地討論這些項(xiàng)目,并嘗試了解是什么使它們在當(dāng)今如此重要。
AlphaGo
2015 年,AlphaGo 成為第一個在19x19
棋盤上擊敗職業(yè)圍棋選手 Lee Sedol 的計(jì)算機(jī)軟件。 突破被記錄下來并作為紀(jì)錄片發(fā)行。 擊敗李·塞多爾的影響如此之大,以至于韓國 Baduk 協(xié)會授予了榮譽(yù) 9 丹證書,這實(shí)際上意味著圍棋選手的游戲技能與神性息息相關(guān)。 這是圍棋歷史上第一次提供 9 榮譽(yù)榮譽(yù)證書,因此提供給 AlphaGo 的證書編號為 001。ELO 等級為 3,739。
AlphaGo Master 的繼任者 AlphaGo Master 在三場比賽中擊敗了當(dāng)時統(tǒng)治世界的游戲冠軍 Ke Jie。 為了表彰這一壯舉,它獲得了中國圍棋協(xié)會頒發(fā)的 9 丹證書。 該軟件當(dāng)時的 ELO 等級為 4,858。
但是,這兩款軟件都被其繼任者 AlphaGo Zero 壓倒了,后者在 3 天的自學(xué)式學(xué)習(xí)中,能夠在 21 分之后以 100:0 的游戲得分擊敗 AlphaGo,在 89:11 的游戲得分下?lián)魯?AlphaGo Master。 天的訓(xùn)練。 40 天后,它的 ELO 評分達(dá)到了 5,185,超過了以前所有 Go AI 的技能。
AlphaGo 基于蒙特卡洛樹搜索算法,并采用了對生成的和人類玩家游戲日志進(jìn)行的深度學(xué)習(xí)。 該模型的初始訓(xùn)練是通過人類游戲進(jìn)行的。 然后,計(jì)算機(jī)將與自己對戰(zhàn)并嘗試改善其游戲性。 樹搜索將被設(shè)置為一定的深度,以避免巨大的計(jì)算開銷,在這種開銷下,計(jì)算機(jī)將嘗試達(dá)到所有可能的動作,然后再進(jìn)行任何動作。
總而言之,遵循以下過程:
- 最初,該模型將在人類游戲日志上進(jìn)行訓(xùn)練。
- 一旦在基線上進(jìn)行了訓(xùn)練,計(jì)算機(jī)將使用在先前步驟中訓(xùn)練過的模型與自己競爭,并使用有上限的蒙特卡洛樹搜索來確保進(jìn)行移動而不會長時間停滯該軟件。 這些游戲的日志已生成。
- 然后對生成的游戲進(jìn)行了訓(xùn)練,從而改善了整體模型。
現(xiàn)在,讓我們討論 Alpha Zero。
Alpha Zero
Alpha Zero 是 AlphaGo Zero 的后繼產(chǎn)品,它是對算法進(jìn)行泛化的嘗試,以便也可以用于其他棋盤游戲。 Alpha Zero 經(jīng)過訓(xùn)練可以下棋,將棋(類似于棋的日式游戲)和圍棋,其表現(xiàn)與相應(yīng)游戲的現(xiàn)有 AI 相當(dāng)。 經(jīng)過 34 小時的訓(xùn)練,Alpha Zero for Go 擊敗了經(jīng)過 3 天訓(xùn)練的 AlphaGo Zero,得分為 60:40。 這導(dǎo)致 ELO 等級為 4,430。
經(jīng)過約 9 個小時的訓(xùn)練,Alpha Zero 擊敗了 TCEC 競賽 2016 年冠軍的 Stockfish 8。 因此,它仍然是迄今為止最強(qiáng)大的國際象棋 AI,盡管有人聲稱最新版本的 Stockfish 將能夠擊敗它。
AlphaGo Zero 和 Alpha Zero 變體之間的主要區(qū)別如下:
- 出現(xiàn)平局的可能性:在圍棋中,保證有一名選手獲勝,而對于象棋則不是這樣。 因此,對 Alpha Zero 進(jìn)行了修改,以允許并列游戲。
- 對稱性:AlphaGo Zero 利用了電路板的對稱性。 但是,由于國際象棋不是非對稱游戲,因此必須對 Alpha Zero 進(jìn)行修改以使其工作。
- 硬編碼的超參數(shù)搜索:Alpha Zero 具有用于超參數(shù)搜索的硬編碼規(guī)則。
- 在 Alpha Zero 的情況下,神經(jīng)網(wǎng)絡(luò)會不斷更新。
此時,您可能會想,“什么是蒙特卡羅樹搜索?”。 讓我們嘗試回答這個問題!
蒙特卡洛樹搜索
當(dāng)我們談?wù)撓笃?#xff0c;圍棋或井字棋等基于當(dāng)前場景的戰(zhàn)略游戲時,我們所談?wù)摰氖谴罅靠赡艿膱鼍昂涂梢栽谌魏吻闆r下在其中的給定點(diǎn)執(zhí)行的動作。 盡管對于井字棋等較小的游戲,可能的狀態(tài)和動作的數(shù)量在現(xiàn)代計(jì)算機(jī)可以計(jì)算的范圍內(nèi),但對于游戲可以生成的狀態(tài)數(shù)量,更復(fù)雜的游戲(如國際象棋和圍棋)呈指數(shù)增長。
蒙特卡洛樹搜索嘗試找到在給定環(huán)境下贏得任何游戲或獲得更好獎勵所需要的正確動作序列。 之所以將其稱為樹搜索是因?yàn)樗鼊?chuàng)建了游戲中所有可能狀態(tài)的樹,并通過創(chuàng)建每個狀態(tài)的分支來實(shí)現(xiàn)其中的所有可能動作。 表示為樹中的節(jié)點(diǎn)。
讓我們考慮以下簡單的游戲示例。 假設(shè)您正在玩一個游戲,要求您猜一個三位數(shù)的數(shù)字,每個猜中都有一個相關(guān)的獎勵。 可能的數(shù)字范圍是 1 到 5,您可以猜測的次數(shù)是 3。 如果您做出準(zhǔn)確的猜測,即正確猜測任意給定位置的數(shù)字,則將獲得 5 分。但是,如果您做出錯誤的猜測,將得到正確數(shù)字兩邊的線性差值的分?jǐn)?shù)。
例如,如果要猜測的數(shù)字是 2,則可能獲得以下獎勵分?jǐn)?shù):
- 如果您猜 1,則得分為 4
- 如果您猜 2,則得分為 5
- 如果您猜 3,則得分為 4
- 如果您猜 4,則得分為 3
- 如果您猜 5,則得分為 2
因此,游戲中的最佳總得分為 15,即每個正確的猜測為 5 分。 鑒于此,您可以在每個步驟中的五個選項(xiàng)中進(jìn)行選擇,游戲中可能的狀態(tài)總數(shù)為5 * 5 * 5 = 125
,只有一個狀態(tài)會給出最佳分?jǐn)?shù)。
讓我們嘗試在樹上描繪前面的游戲。 假設(shè)您要猜測的數(shù)字是 413。在第一步中,您將具有以下樹:
做出選擇后,您將獲得獎勵,再次有五個選項(xiàng)可供選擇-換句話說,每個節(jié)點(diǎn)中有五個分支可以遍歷。 在最佳游戲玩法中,將獲得以下樹:
現(xiàn)在,讓我們考慮以下事實(shí):圍棋游戲共有3^361
個可能狀態(tài)。 在 AI 采取行動之前嘗試計(jì)算每種可能性變得不切實(shí)際。 這是蒙特卡羅樹搜索與上限可信度算法相結(jié)合的地方,它比其他方法更具優(yōu)勢,因?yàn)樗梢越K止到任何搜索深度,并且可以產(chǎn)生趨向于最佳分?jǐn)?shù)的結(jié)果。 因此,算法不需要遍歷樹的每個分支。 一旦樹形搜索算法意識到任何特定分支的表現(xiàn)不佳,就可以停止沿該路徑前進(jìn),而專注于表現(xiàn)更好的路徑。 而且,它可以盡早終止任何路徑并在該點(diǎn)返回預(yù)期的回報,從而可以調(diào)整 AI 采取任何行動所需的時間。
更確切地說,蒙特卡羅樹搜索遵循以下步驟:
-
選擇:從樹的當(dāng)前節(jié)點(diǎn)中選擇最佳回報分支。 例如,在前面的游戲樹中,選擇除 4 以外的任何分支將產(chǎn)生較低的分?jǐn)?shù),因此選擇了 4。
-
擴(kuò)展:一旦選擇了最佳回報節(jié)點(diǎn),該節(jié)點(diǎn)下的樹將進(jìn)一步擴(kuò)展,從而創(chuàng)建具有該節(jié)點(diǎn)可用的所有可能選項(xiàng)(分支)的節(jié)點(diǎn)。 這可以理解為從游戲的任何位置布局 AI 的未來動作。
-
模擬:現(xiàn)在,由于事先不知道在擴(kuò)展階段創(chuàng)建的哪個未來選項(xiàng)最有回報,因此我們使用強(qiáng)化學(xué)習(xí)逐個模擬游戲的每個選項(xiàng)。 請注意,與上限可信度上限算法結(jié)合使用時,直到結(jié)束游戲才算重要。 計(jì)算任何
n
個步驟的獎勵也是一種不錯的方法。 -
更新:最后,更新節(jié)點(diǎn)和父節(jié)點(diǎn)的獎勵分?jǐn)?shù)。 盡管不可能回到游戲中,并且由于任何節(jié)點(diǎn)的值都已減小,但如果在以后的游戲中的那個階段找到了更好的替代方案,那么 AI 將不會遵循這條路徑,從而通過多次迭代來改善其游戲玩法。
接下來,我們將構(gòu)建一個系統(tǒng),該系統(tǒng)的工作原理類似于 Alpha Zero,并嘗試學(xué)習(xí)玩 Connect 4 游戲,該游戲比 Tic-Tac-Toe 游戲要復(fù)雜得多,但對我們來說足夠大,來解釋如何構(gòu)建類似的國際象棋引擎。
適用于 Connect 4 的類似 Alpha Zero 的 AI
在開始研究可玩 Connect4 的 AI 之前,讓我們簡要了解一下游戲及其動態(tài)。 Connect 4,有時也稱為連續(xù)四人,連續(xù)四人,四人以上,等等,是全世界兒童中最受歡迎的棋盤游戲之一。 我們也可以將它理解為井字棋的更高級版本,在其中您必須水平,垂直或?qū)欠胖萌齻€相同類型的標(biāo)記。 棋盤通常是一個6x7
的網(wǎng)格,兩個玩家各自玩一個標(biāo)記。
Connect 4 的規(guī)則可能會有所不同,因此讓我們?yōu)?AI 將學(xué)習(xí)的規(guī)則版本制定一些具體規(guī)則:
- 該游戲被模擬為在具有七個空心列和六行的垂直板上玩。 每列在板的頂部都有一個開口,可以在其中插入片段。可以查看已放入板的片段。
- 兩位玩家都有 21 個形狀像不同顏色硬幣的硬幣。
- 將硬幣放在板上構(gòu)成一個動作。
- 碎片從頂部的開口下降到最后一行,或者堆積在該列的最后一塊。
- 第一個以任意方向連接其任意四枚硬幣的玩家,因此彼此之間不會存在任何間隙或其他玩家的硬幣獲勝。
現(xiàn)在,讓我們分解將 Connect 4 播放式自學(xué) AI 分解為子問題的問題:
- 首先,我們需要創(chuàng)建棋盤的虛擬表示。
- 接下來,我們必須創(chuàng)建允許根據(jù)游戲規(guī)則移動的函數(shù)。
- 然后,為了保存游戲狀態(tài),我們需要一個狀態(tài)管理系統(tǒng)。
- 接下來,我們將簡化游戲玩法,其中將提示用戶進(jìn)行移動并宣布游戲終止。
- 之后,我們必須創(chuàng)建一個腳本,該腳本可以生成示例游戲玩法,供系統(tǒng)學(xué)習(xí)。
- 然后,我們必須創(chuàng)建訓(xùn)練函數(shù)來訓(xùn)練系統(tǒng)。
- 接下來,我們需要蒙特卡洛樹搜索(MCTS)實(shí)現(xiàn)。
- 最后,我們需要一個神經(jīng)網(wǎng)絡(luò)的實(shí)現(xiàn)。
- 除了前面的具體步驟之外,我們還需要為系統(tǒng)創(chuàng)建許多驅(qū)動腳本以使其更加可用。
讓我們依次移至前面的要點(diǎn),一次覆蓋系統(tǒng)的每個部分。 但是,首先,我們將快速瀏覽該項(xiàng)目中存在的目錄結(jié)構(gòu)和文件,這在本書的 GitHub 存儲庫中也可以找到。 讓我們來看看:
command/
:__init__.py
:此文件使我們可以將此文件夾用作模塊。arena.py
:此文件獲取并解析用于運(yùn)行游戲的命令。generate.py
:此文件接受并分析自玩招式生成系統(tǒng)的命令。newmodel.py
:此文件用于為智能體創(chuàng)建新的空白模型。train.py
:此文件用于訓(xùn)練基于增強(qiáng)學(xué)習(xí)的神經(jīng)網(wǎng)絡(luò)如何玩游戲。util/
:__init__.py
:此文件使我們可以將此文件夾用作模塊。arena.py
:此文件創(chuàng)建并維護(hù)玩家之間進(jìn)行的比賽的記錄,并允許我們在輪到誰之間切換。compat.py
:此文件是用于使程序與 Python 2 和 Python 3 兼容的便捷工具。如果您確定正在開發(fā)的版本并希望在其上運(yùn)行,則可以跳過此文件。generate.py
:此文件播放一些隨機(jī)移動的游戲,再加上 MCTS 移動,以生成可用于訓(xùn)練目的的游戲日志。 該文件存儲每個游戲的獲勝者以及玩家做出的動作。internal.py
:此文件創(chuàng)建棋盤的虛擬表示并定義與棋盤相關(guān)的函數(shù),例如將棋子放置在棋盤上,尋找獲勝者或只是創(chuàng)建新棋盤。keras_model.py
:此文件定義充當(dāng)智能體大腦的模型。 在本項(xiàng)目的后面,我們將更深入地討論該文件。mcts.py
:此文件提供 MCTS 類,該類實(shí)質(zhì)上是蒙特卡羅樹搜索的實(shí)現(xiàn)。nn.py
:此文件提供 NN 類,它是神經(jīng)網(wǎng)絡(luò)的實(shí)現(xiàn),以及與神經(jīng)網(wǎng)絡(luò)相關(guān)的函數(shù),例如擬合,預(yù)測,保存等。player.py
:此文件為兩種類型的播放器提供了類-MCTS 播放器和人工播放器。 MCTS 玩家是我們將訓(xùn)練的智能體,以玩游戲。state.py
:這是internal.py
文件的包裝,提供了用于訪問電路板和與電路板相關(guān)的函數(shù)的類。trainer.py
:這使我們可以訓(xùn)練模型。 這與nn.py
中提供的內(nèi)容不同,因?yàn)樗鼘W⒂诤w游戲的訓(xùn)練過程,而nn.py
中的內(nèi)容主要是圍繞此功能的包裝。
接下來,我們將繼續(xù)探索這些文件中每個文件的一些重要部分,同時遵循我們先前為構(gòu)建 AI 制定的步驟。
創(chuàng)建棋盤的虛擬表示
您將如何代表 Connect 4 棋盤? 代表 Connect 4 棋盤的兩種常用方法以及游戲狀態(tài)。 讓我們來看看:
- 人類可讀的長格式:在這種形式中,木板的行和列分別顯示在 x 和 y 軸上,并且兩個玩家的標(biāo)記都顯示為
x
和o
, 分別(或任何其他合適的字符)。 可能如下所示:
|1 2 3 4 5 6 7
--+--------------1|. . . . . . .2|. . . . . . .3|. . . . . . .4|. . . . o x .5|x o x . o o .6|o x x o x x o
但是,這種形式有點(diǎn)冗長并且在計(jì)算上不是很友好。
- 計(jì)算有效的形式:在此形式中,我們將板存儲為 2D NumPy 數(shù)組:
array([[1, 1, 0, 0, 0, 0, 0],[0, 0, 0, 0, 0, 0, 0],[0, 0, 0, 0, 0, 0, 0],[0, 0, 0, 0, 0, 0, 0],[0, 0, 0, 0, 1, 0, 0],[0, 0, 0, 0, 0, 0, 0]], dtype=int8)
以這種方式創(chuàng)建該數(shù)組,當(dāng)將其展平為一維數(shù)組時,板位置按順序排列,就好像該數(shù)組實(shí)際上是一維數(shù)組一樣。 前兩個位置分別編號為 0 和 1,而第 5 個位置位于第 5 行和第 5 列,編號為 32。通過將前一個代碼塊中的矩陣與給定的表進(jìn)行映射,可以輕松理解此條件。 在下圖中:
這種形式適合于進(jìn)行計(jì)算,但不適合玩家在游戲過程中觀看,因?yàn)閷τ谕婕叶院茈y解密。
- 一旦決定了如何表示電路板及其部件,就可以開始在
util/internal.py
文件中編寫代碼,如下所示:
BOARD_SIZE_W = 7
BOARD_SIZE_H = 6
KEY_SIZE = BOARD_SIZE_W * BOARD_SIZE_H
前幾行設(shè)置了板子的常數(shù),在這種情況下,是板子上的行數(shù)和列數(shù)。 我們還通過將它們相乘來計(jì)算板上的按鍵或位置的數(shù)量。
- 現(xiàn)在,讓我們準(zhǔn)備在板上生成獲勝位置的代碼,如下所示:
LIST4 = []
LIST4 += [[(y, x), (y + 1, x + 1), (y + 2, x + 2), (y + 3, x + 3)] for y in range(BOARD_SIZE_H - 3) for x in range(BOARD_SIZE_W - 3)]
LIST4 += [[(y, x + 3), (y + 1, x + 2), (y + 2, x + 1), (y + 3, x)] for y in range(BOARD_SIZE_H - 3) for x in range(BOARD_SIZE_W - 3)]
LIST4 += [[(y, x), (y, x + 1), (y, x + 2), (y, x + 3)] for y in range(BOARD_SIZE_H) for x in range(BOARD_SIZE_W - 3)]
NO_HORIZONTAL = len(LIST4)
LIST4 += [[(y, x), (y + 1, x), (y + 2, x), (y + 3, x)] for y in range(BOARD_SIZE_H - 3) for x in range(BOARD_SIZE_W)]
LIST4
變量存儲任何玩家贏得比賽時可以實(shí)現(xiàn)的可能組合。
我們不會在此文件中討論整個代碼。 但是,重要的是要了解以下函數(shù)及其作用:
get_start_board()
:此函數(shù)以 NumPy 數(shù)組的形式返回電路板的空白 2D 數(shù)組表示形式。clone_board(board)
:此函數(shù)用于按板級克隆整個 NumPy 數(shù)組。get_action(board)
:此函數(shù)返回播放器已修改的數(shù)組中的位置。action_to_string(action)
:此函數(shù)將玩家執(zhí)行的動作的內(nèi)部數(shù)字表示形式轉(zhuǎn)換為可以以易于理解的形式顯示給用戶的字符串。 例如place_at(board, pos,
。player)
:執(zhí)行為任何給定玩家在板上放置一塊棋子的動作。 它還會更新板。def get_winner(board)
:此函數(shù)確定棋盤當(dāng)前狀態(tài)下的游戲是否有贏家。 如果是,則返回獲勝玩家的標(biāo)識符,該標(biāo)識符將為 1 或 -1。def to_string(board)
:此函數(shù)將板的 NumPy 數(shù)組表示形式轉(zhuǎn)換為字符串,該字符串為人類可讀的格式。
接下來,我們將研究如何對 AI 進(jìn)行編程,使其根據(jù)游戲規(guī)則進(jìn)行并僅接受有效的動作。
允許根據(jù)游戲規(guī)則移動
為了確定玩家(無論是人還是機(jī)器)做出的動作的有效性,我們需要建立一種機(jī)制,在機(jī)器的情況下,該機(jī)制連續(xù)不斷地只生成有效的動作,或者不斷驗(yàn)證任何人類玩家的輸入。 讓我們開始吧:
- 可以在
util/generator.py
文件的_selfplay(self, state, args)
函數(shù)中找到一個這樣的實(shí)例,如以下代碼所示:
turn = 0
hard_random_turn = args['hard_random'] if 'hard_random' in args else 0
soft_random_turn = (args['soft_random'] if 'soft_random' in args else 30) + hard_random_turn
history = []
首先,我們將移動切換設(shè)置為0
,指示游戲開始時尚未進(jìn)行任何移動。 我們還考慮了用戶在其 AI 自行生成的游戲中想要的硬性和軟性隨機(jī)回合的數(shù)量。 然后,我們將移動的歷史記錄設(shè)置為空白。
- 現(xiàn)在,我們可以開始為 AI 生成動作,如下所示:
while state.getWinner() == None:if turn < hard_random_turn:# random actionaction_list = state.getAction()index = np.random.choice(len(action_list))(action, key) = action_list[index]
前面的代碼說,直到?jīng)]有游戲的獲勝者,都必須生成招式。 在前面的案例中,我們可以看到,只要進(jìn)行一次隨機(jī)隨機(jī)轉(zhuǎn)彎的可能性為真,AI 就會選擇一個完全隨機(jī)的位置來放置其棋子。
- 通過在前面的
if
語句中添加else
塊,我們告訴 AI,只要它需要進(jìn)行柔和轉(zhuǎn)彎,它就可以檢查是否有任何隨機(jī)位置將其放置在其中,但只能在 MCTS 算法所建議的移動范圍內(nèi),如下所示:
else:action_list = self.mcts.getActionInfo(state, args['simulation'])if turn < soft_random_turn:# random action by visited countvisited = [1.0 * a.visited for a in action_list]sum_visited = sum(visited)assert(sum_visited > 0)p = [v / sum_visited for v in visited]index = np.random.choice(len(action_list), p = p)else:# select most visited countindex = np.argmax([a.visited for a in action_list])
請注意,如果既不進(jìn)行硬轉(zhuǎn)彎也不進(jìn)行軟轉(zhuǎn)彎,則坐席會在游戲的那一刻進(jìn)行最常用的動作,這有望使它朝著勝利邁進(jìn)。
因此,在非人類玩家的情況下,智能體只能在任何給定階段在一組填充的有效動作之間進(jìn)行選擇。 對于人類玩家而言,情況并非如此,根據(jù)他們的創(chuàng)造力,他有可能嘗試做出無效的舉動。 因此,當(dāng)人類玩家做出動作時,需要對其進(jìn)行驗(yàn)證。
- 可以在
util/player.py
文件的getNextAction(self, state)
函數(shù)中找到驗(yàn)證人類玩家移動的方法,如下所示:
action = state.getAction()
available_x = []
for i in range(len(action)):a, k = action[i]x = a % util.BOARD_SIZE_W + 1y = a // util.BOARD_SIZE_W + 1print('{} - {},{}'.format(x, x, y))available_x.append(x)
- 首先,我們現(xiàn)在計(jì)算人類玩家可能采取的合法行動,并將其顯示給用戶。 然后,我們提示用戶輸入一個動作,直到他們做出有效的動作為止,如下所示:
while True:try:x = int(compat_input('enter x: '))if x in available_x:for i in range(len(action)):if available_x[i] == x:select = ibreakbreakexcept ValueError:pass
因此,我們根據(jù)填充的一組有效動作來驗(yàn)證用戶所做的動作。 我們還可以選擇向用戶顯示錯誤。
接下來,我們將研究程序的狀態(tài)管理系統(tǒng),您肯定已經(jīng)注意到,到目前為止,我們一直在看該代碼。
狀態(tài)管理系統(tǒng)
游戲的狀態(tài)管理系統(tǒng)是整個程序中最重要的部分之一,因?yàn)樗刂浦械挠螒蛲娣?#xff0c;并在 AI 的自學(xué)習(xí)過程中促進(jìn)了游戲玩法。 這樣可以確保向玩家展示棋盤,并在進(jìn)行有效的移動。 它還存儲了幾個與狀態(tài)有關(guān)的變量,這些變量對于游戲進(jìn)行很有用。 讓我們來看看:
- 讓我們討論
util/state.py
文件中提供的State
類中最重要的特性和函數(shù):
import .internal as util
此類使用util/internal.py
文件中定義的名稱為util
的變量和函數(shù)。
__init__(self, prototype = None)
:此類在啟動時,會繼承現(xiàn)有狀態(tài)或創(chuàng)建新狀態(tài)。 該函數(shù)的定義如下:
def __init__(self, prototype = None):if prototype == None:self.board = util.get_start_board()self.currentPlayer = 1self.winner = Noneelse:self.board = util.clone_board(prototype.board)self.currentPlayer = prototype.currentPlayerself.winner = prototype.winner
在這里,您可以看到該類可以使用游戲的現(xiàn)有狀態(tài)啟動,并作為參數(shù)傳遞給該類的構(gòu)造器; 否則,該類將創(chuàng)建一個新的游戲狀態(tài)。
getRepresentativeString(self)
:此函數(shù)返回可以由人類玩家讀取的游戲狀態(tài)的格式正確的字符串表示形式。 其定義如下:
def getRepresentativeString(self):return ('x|' if self.currentPlayer > 0 else 'o|') + util.to_oneline(self.board)
狀態(tài)類中的許多其他重要方法如下:
getCurrentPlayer(self)
:此方法返回游戲的當(dāng)前玩家; 也就是說,應(yīng)該采取行動的玩家。getWinner(self)
:如果游戲結(jié)束,則此方法返回游戲獲勝者的標(biāo)識符。getAction(self)
:此方法檢查游戲是否結(jié)束。 如果沒有,它將在任何給定狀態(tài)下返回一組下一個可能的動作。getNextState(self, action)
:此方法返回游戲的下一個狀態(tài); 也就是說,在將當(dāng)前正在移動的棋子放在棋盤上并評估游戲是否結(jié)束之后,它將執(zhí)行從一種狀態(tài)到另一種狀態(tài)的切換。getNnInput(self)
:此方法返回玩家到目前為止在游戲中執(zhí)行的動作,并為每個玩家的動作使用不同的標(biāo)記。
現(xiàn)在,讓我們看一下如何改善程序的游戲玩法。
實(shí)現(xiàn)游戲玩法
負(fù)責(zé)控制程序中游戲玩法的文件是util/arena.py
文件。
它在Arena
類中定義了以下兩種方法:
def fight(self, state, p1, p2, count):stats = [0, 0, 0]for i in range(count):print('==== EPS #{} ===='.format(i + 1))winner = self._fight(state, p1, p2)stats[winner + 1] += 1print('stats', stats[::-1])winner = self._fight(state, p2, p1)stats[winner * -1 + 1] += 1print('stats', stats[::-1])
前面的fight()
函數(shù)管理玩家的勝利/損失或平局的狀態(tài)。 它確保在每個回合中進(jìn)行兩場比賽,其中每位玩家只能先玩一次。
此類中定義的另一個_fight()
函數(shù)如下:
def _fight(self, state, p1, p2):while state.getWinner() == None:print(state)if state.getCurrentPlayer() > 0:action = p1.getNextAction(state)else:action = p2.getNextAction(state)state = state.getNextState(action)print(state)return state.getWinner()
此函數(shù)負(fù)責(zé)切換棋盤上的玩家,直到找到贏家為止。
現(xiàn)在,讓我們看一下如何生成隨機(jī)的游戲玩法以使智能體自學(xué)。
生成示例游戲
到目前為止,我們已經(jīng)討論了util/gameplay.py
文件,以演示該文件中與移動規(guī)則相關(guān)的代碼-特別是該文件的自播放函數(shù)。 現(xiàn)在,我們來看看這些自玩游戲如何在迭代中運(yùn)行以生成完整的游戲玩法日志。 讓我們開始吧:
- 請考慮此文件提供的
Generator
類的generate()
方法的代碼:
def generate(self, state, nn, cb, args):self.mcts = MCTS(nn)iterator = range(args['selfplay'])if args['progress']:from tqdm import tqdmiterator = tqdm(iterator, ncols = 50)# self playfor pi in iterator:result = self._selfplay(state, args)if cb != None:cb(result)
本質(zhì)上,此函數(shù)負(fù)責(zé)運(yùn)行該類的_selfplay()
函數(shù),并確定一旦完成自播放后必須執(zhí)行的操作。 在大多數(shù)情況下,您會將輸出保存到文件中,然后將其用于訓(xùn)練。
- 這已在
command/generate.py
文件中定義。 該腳本可以作為具有以下簽名的命令運(yùn)行:
usage: run.py generate [-h][--model, default='latest.h5', help='model filename'][--number, default=1000000, help='number of generated states'][--simulation, default=100, help='number of simulations per move'][--hard, default=0, help='number of random moves'][--soft, default=1000, help='number of random moves that depends on visited node count'][--progress, help='show progress bar'][--gpu, help='gpu memory fraction'][--file, help='save to a file'][--network, help='save to remote server']
- 該命令的示例調(diào)用如下:
python run.py generate --model model.h5 --simulation 100 -n 5000 --file selfplay.txt --progress
現(xiàn)在,讓我們看一下一旦生成自播放日志就可以訓(xùn)練模型的函數(shù)。
系統(tǒng)訓(xùn)練
要訓(xùn)??練智能體,我們需要創(chuàng)建util/trainer.py
文件,該文件提供train()
函數(shù)。 讓我們來看看:
- 簽名如下:
train(state, nn, filename, args = {})
該函數(shù)接受State
類,神經(jīng)網(wǎng)絡(luò)類和其他參數(shù)。 它還接受文件名,該文件名是包含生成的游戲玩法的文件的路徑。 訓(xùn)練后,我們可以選擇將輸出保存到另一個模型文件中,如command/train.py
文件的train()
函數(shù)所提供的。
- 此命令具有以下簽名:
usage: run.py train [-h][--progress, help='show progress bar'][--epoch EPOCH, help='training epochs'][--batch BATCH, help='batch size'][--block BLOCK, help='block size'][--gpu GPU, help='gpu memory fraction']history, help='history file'input, help='input model file name'output, help='output model file name'
歷史參數(shù)是存儲生成的游戲玩法的文件。 輸入文件是當(dāng)前保存的模型文件,而輸出文件是將新訓(xùn)練的模型保存到的文件。
- 該命令的示例調(diào)用如下:
python run.py train selfplay.txt model.h5 newmodel.h5 --epoch 3 --progress
現(xiàn)在我們已經(jīng)有了一個訓(xùn)練系統(tǒng),我們需要創(chuàng)建 MCTS 和神經(jīng)網(wǎng)絡(luò)實(shí)現(xiàn)。
實(shí)現(xiàn)蒙特卡羅樹搜索
util/mcts.py
文件中提供了完整的 MCTS 算法實(shí)現(xiàn)。 該文件提供了 MCTS 類,該類具有以下重要函數(shù):
getMostVisitedAction
:此函數(shù)返回將狀態(tài)傳遞給訪問次數(shù)最多的操作。getActionInfo
:執(zhí)行任何操作后,此函數(shù)返回狀態(tài)信息。_simulation
:此函數(shù)執(zhí)行單個游戲模擬,并返回有關(guān)在模擬過程中玩過的游戲的信息。
最后,我們需要創(chuàng)建一個神經(jīng)網(wǎng)絡(luò)實(shí)現(xiàn)。
實(shí)現(xiàn)神經(jīng)網(wǎng)絡(luò)
在最后一節(jié)中,我們將了解為智能體進(jìn)行訓(xùn)練而創(chuàng)建的神經(jīng)網(wǎng)絡(luò)。 我們將探索util/nn.py
文件,該文件提供NN
類以及以下重要方法:
__init__(self, filename)
:如果磁盤上不存在此函數(shù),則使用util/keras_model.py
函數(shù)創(chuàng)建新模型。 否則,它將模型文件加載到程序中。util/keras_model.py
文件中定義的模型是殘差 CNN,它與 MCTS 和 UCT 結(jié)合使用,表現(xiàn)得像深度強(qiáng)化學(xué)習(xí)神經(jīng)網(wǎng)絡(luò)。 形成的模型具有以下配置:
input_dim: (2, util.BOARD_SIZE_H, util.BOARD_SIZE_W),
policy_dim: util.KEY_SIZE,
res_layer_num: 5,
cnn_filter_num: 64,
cnn_filter_size: 5,
l2_reg: 1e-4,
learning_rate: 0.003,
momentum: 0.9
默認(rèn)情況下,模型具有五個殘差卷積層塊。 我們先前在util/internal.py
文件中定義了BOARD_SIZE_H
,BOARD_SIZE_W
和KEY_SIZE
常量:
save(self, filename)
:此函數(shù)將模型保存到提供的文件名中。predict(self, x)
:提供了板狀態(tài)以及已經(jīng)進(jìn)行的移動,此函數(shù)輸出可以下一步進(jìn)行的單個移動。fit(self, x, policy, value, batch_size = 256, epochs = 1)
:此函數(shù)用于將新樣本擬合到模型并更新權(quán)重。
除了上述腳本之外,我們還需要一些驅(qū)動腳本。 您可以在該項(xiàng)目的存儲庫中查找它們,以了解它們的用法。
要運(yùn)行已完成的項(xiàng)目,您需要執(zhí)行以下步驟:
- 使用以下命令創(chuàng)建新模型:
python run.py newmodel model.h5
這將創(chuàng)建一個新模型并打印出其摘要。
- 生成示例游戲日志:
python run.py generate --model model.h5 --simulation 100 -n 5000 --file selfplay.txt --progress
在仿真過程中,上一行為 MCTS 生成了 5,000 個示例游戲,深度為 100。
- 訓(xùn)練模型:
python run.py train selfplay.txt model.h5 newmodel.h5 --epoch 3 --progress
前面的命令在游戲文件上訓(xùn)練模型三個時間,并將訓(xùn)練后的模型另存為newmodel.h5
。
- 與 AI 對抗:
python run.py arena human mcts,newmodel.h5,100
前面的命令開始與 AI 進(jìn)行游戲。 在這里,您將在終端中看到一個面板和游戲選項(xiàng),如下所示:
現(xiàn)在,我們已經(jīng)成功創(chuàng)建了一個基于 Alpha Zero 的程序來學(xué)習(xí)玩棋盤游戲,現(xiàn)在我們可以將其推論到國際象棋 AI 上了。 但是,在這樣做之前,我們將簡要地介紹項(xiàng)目架構(gòu)。
基礎(chǔ)項(xiàng)目架構(gòu)
為了創(chuàng)建國際象棋引擎,將其作為 REST API 托管在 GCP 上,我們將遵循常規(guī)項(xiàng)目架構(gòu):
雖然上圖提供了該項(xiàng)目的非常簡化的概述,但它可以用于更復(fù)雜的系統(tǒng),這些系統(tǒng)可以產(chǎn)生更好的自學(xué)習(xí)象棋引擎。
GCP 上托管的模型將放置在 EC2 VM 實(shí)例中,并將包裝在基于 Flask 的 REST API 中。
為國際象棋引擎開發(fā) GCP 托管的 REST API
現(xiàn)在我們已經(jīng)看到了如何繼續(xù)進(jìn)行此項(xiàng)目,我們還需要討論如何將 Connect 4 的游戲映射到國際象棋,以及如何將國際象棋 RL 引擎部署為 API。
您可以在這個頁面上找到我們?yōu)樵撓笃逡鎰?chuàng)建的文件。 在將這些文件與 Connect 4 項(xiàng)目中的文件映射之前,讓我們快速了解一些最重要的文件:
src/chess_zero/agent/
:player_chess.py
:此文件描述ChessPlayer
類,該類保存有關(guān)在任何時間點(diǎn)玩游戲的玩家的信息。 它為與使用蒙特卡洛樹搜索來搜索新動作,更改玩家狀態(tài)以及每個用戶在玩游戲期間所需的其他功能的相關(guān)方法提供了包裝。model_chess.py
:此文件描述了此系統(tǒng)中使用的剩余 CNN。src/chess_zero/config/
:mini.py
:此文件定義國際象棋引擎學(xué)習(xí)或玩的配置。 您將需要在此處有時調(diào)整這些參數(shù),以降低在低端計(jì)算機(jī)上進(jìn)行訓(xùn)練期間的批量大小或虛擬 RAM 消耗。src/chess_zero/env/
:chess_env.py
:此文件描述棋盤的設(shè)置,游戲規(guī)則以及執(zhí)行游戲操作所需的函數(shù)。 它還包含檢查游戲狀態(tài)和驗(yàn)證移動的方法。src/chess_zero/worker/
:evaluate.py
:此文件負(fù)責(zé)與當(dāng)前最佳模型和下一代模型玩游戲。 如果下一代模型的表現(xiàn)優(yōu)于 100 款游戲,則它將替代以前的模型。optimize.py
:此文件加載當(dāng)前最佳模型,并在其上執(zhí)行更多監(jiān)督的基于學(xué)習(xí)的訓(xùn)練。self.py
:引擎與自己對戰(zhàn)并學(xué)習(xí)新的游戲玩法。sl.py
:監(jiān)督學(xué)習(xí)的縮寫,此文件將來自其他玩家的游戲的 PGN 文件作為輸入,并對其進(jìn)行監(jiān)督學(xué)習(xí)。src/chess_zero/play_game/
:uci.py
:此文件提供了通用國際象棋界面(UCI)標(biāo)準(zhǔn)環(huán)境,可以與引擎進(jìn)行交互。flask_server.py
:該文件創(chuàng)建一個 Flask 服務(wù)器,該服務(wù)器使用國際象棋游戲的 UCI 表示法與引擎進(jìn)行通信。
現(xiàn)在我們知道每個文件的作用,讓我們建立這些文件與 Connect 4 游戲中文件的映射。
還記得我們在討論 Connect 4 AI 時制定的步驟嗎? 讓我們看看國際象棋項(xiàng)目是否也遵循相同的步驟:
- 創(chuàng)建棋盤的虛擬代表。 這是在
src/chess_zero/env/chess_env.py
文件中完成的。 - 創(chuàng)建允許根據(jù)游戲規(guī)則進(jìn)行移動的函數(shù)。 這也可以在
src/chess_zero/env/chess_env.py
文件中完成。 - 原地的狀態(tài)管理系統(tǒng):此功能在許多文件上維護(hù),例如
src/chess_zero/agent/player_chess.py
和src/chess_zero/env/chess_env.py
。 - 簡化游戲:這是通過
src/chess_zero/play_game/uci.py
文件完成的。 - 創(chuàng)建一個可以生成示例游戲玩法的腳本,以供系統(tǒng)學(xué)習(xí)。 盡管此系統(tǒng)未將生成的游戲玩法明確地存儲為磁盤上的文件,但該任務(wù)由
src/chess_zero/worker/self_play.py
執(zhí)行。 - 創(chuàng)建訓(xùn)練函數(shù)來訓(xùn)練系統(tǒng)。 這些訓(xùn)練函數(shù)位于
src/chess_zero/worker/sl.py
和src/chess_zero/worker/self.py
處。 - 現(xiàn)在,我們需要一個 MCTS 實(shí)現(xiàn)。 可以在
src/chess_zero/agent/player_chess.py
的文件的移動搜索方法中找到該項(xiàng)目的 MCTS 實(shí)現(xiàn)。 - 神經(jīng)網(wǎng)絡(luò)的實(shí)現(xiàn):
src/chess_zero/agent/model_chess.py
中定義了項(xiàng)目的神經(jīng)網(wǎng)絡(luò)。
除了前面的映射之外,我們還需要討論 Universal Chess Interface 和 Flask 服務(wù)器腳本,這兩個都是游戲性和 API 部署所必需的。
了解通用國際象棋界面
/src/chess_zero/play_game/uci.py
上的文件為引擎創(chuàng)建了通用國際象棋界面。 但是,UCI 到底是什么?
UCI 是 Rudolf Huber 和 Stefan Meyer-Kahlen 引入的一種通信標(biāo)準(zhǔn),它允許在任何控制臺環(huán)境中使用國際象棋引擎進(jìn)行游戲。 該標(biāo)準(zhǔn)使用一小組命令來調(diào)用國際象棋引擎,以搜索并輸出板子任何給定位置的最佳動作。
通過 UCI 進(jìn)行的通信與標(biāo)準(zhǔn)輸入/輸出發(fā)生,并且與平臺無關(guān)。 在我們程序的 UCI 腳本中可用的命令如下:
uci
:打印正在運(yùn)行的引擎的詳細(xì)信息。isready
:這查詢引擎是否準(zhǔn)備好進(jìn)行對抗。ucinewgame
:這將啟動帶有引擎的新游戲。position [fen | startpos] moves
:此設(shè)置板的位置。 如果用戶從非起始位置開始,則用戶需要提供 FEN 字符串來設(shè)置板。go
:這要求引擎進(jìn)行搜索并提出最佳建議。quit
:這將結(jié)束游戲并退出界面。
以下代碼顯示了帶有 UCI 引擎的示例游戲玩法:
> uci
id name ChessZero
id author ChessZero
uciok> isready
readyok> ucinewgame> position startpos moves e2e4> go
bestmove e7e5> position rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 1 moves g1f3> go
bestmove b8c6> quit
要快速生成任何板位置的 FEN 字符串,可以使用板編輯器。
現(xiàn)在,讓我們討論一下 Flask 服務(wù)器腳本以及如何在 GCP 實(shí)例上部署它。
在 GCP 上部署
該國際象棋引擎程序需要存在 GPU。 因此,我們必須遵循其他步驟,才能在 GCP 實(shí)例上部署腳本。
大致的工作流程如下:
- 請求增加帳戶可用的 GPU 實(shí)例的配額。
- 創(chuàng)建基于 GPU 的計(jì)算引擎實(shí)例。
- 部署腳本。
我們將在以下各節(jié)中詳細(xì)介紹這些步驟。
請求增加 GPU 實(shí)例的配額
第一步將是請求增加 GPU 實(shí)例的配額。 默認(rèn)情況下,您的 GCP 帳戶上可擁有的 GPU 實(shí)例數(shù)為 0。此限制由您的帳戶的配額配置設(shè)置,您需要請求增加。 這樣做,請按照下列步驟操作:
- 通過這里打開 Goog??le Cloud Platform 控制臺。
- 在左側(cè)菜單上,單擊“IAM&Admin | 配額”,如以下屏幕截圖所示:
- 單擊
Metrics
過濾器,然后鍵入 GPU 以找到讀取 GPU(所有區(qū)域)的條目,如以下屏幕截圖所示:
- 選擇條目,然后單擊“編輯配額”。
- 系統(tǒng)將要求您提供身份證明,包括您的電話號碼。 填寫詳細(xì)信息,然后單擊“下一步”。
- 輸入您想要將 GPU 配額設(shè)置為的限制(最好是
1
,以避免濫用)。 另外,請?zhí)峁┠岢鲆蟮睦碛?#xff0c;例如學(xué)術(shù)研究,機(jī)器學(xué)習(xí)探索或任何適合您的東西! - 單擊“提交”。
提出要求后,大約需要 10 到 15 分鐘才能將您的配額增加/設(shè)置為您指定的數(shù)量。 您將收到一封電子郵件,通知您有關(guān)此更新。 現(xiàn)在,您準(zhǔn)備創(chuàng)建一個 GPU 實(shí)例。
創(chuàng)建一個 GPU 實(shí)例
下一步是創(chuàng)建 GPU 實(shí)例。 創(chuàng)建 GPU 實(shí)例的過程與創(chuàng)建非 GPU 實(shí)例的過程非常相似,但是需要額外的步驟。 讓我們快速完成所有這些步驟:
- 在您的 Google Cloud Platform 儀表板上,單擊左側(cè)導(dǎo)航菜單中的“Compute Engine | VM 實(shí)例”。
- 單擊“創(chuàng)建實(shí)例”。
- 單擊“計(jì)算機(jī)類型選擇”部分正下方的 CPU 平臺和 GPU,如以下屏幕截圖所示:
- 單擊“添加 GPU”(大加號(
+
)按鈕)。 選擇要附加到此 VM 的 GPU 類型和 GPU 數(shù)量。 - 將啟動盤操作系統(tǒng)更改為 Ubuntu 版本 10.10。
- 在“防火墻”部分中,檢查 HTTP 和 HTTPS 通信權(quán)限,如以下屏幕截圖所示:
- 單擊表單底部的“創(chuàng)建”。
幾秒鐘后,您的實(shí)例將成功創(chuàng)建。 如果遇到任何錯誤,例如超出了區(qū)域資源限制,請嘗試更改要在其中創(chuàng)建實(shí)例的區(qū)域/區(qū)域。這通常是一個臨時問題。
現(xiàn)在,我們可以部署 Flask 服務(wù)器腳本。
部署腳本
現(xiàn)在,我們將部署 Flask 服務(wù)器腳本。 但是在我們這樣做之前,讓我們先看一下該腳本的作用:
- 腳本的前幾行導(dǎo)入了必要的模塊,腳本才能正常工作:
from flask import Flask, request, jsonify
import os
import sys
import multiprocessing as mp
from logging import getLoggerfrom chess_zero.agent.player_chess import ChessPlayer
from chess_zero.config import Config, PlayWithHumanConfig
from chess_zero.env.chess_env import ChessEnvfrom chess_zero.agent.model_chess import ChessModel
from chess_zero.lib.model_helper import load_best_model_weightlogger = getLogger(__name__)
- 其余代碼放入
start()
函數(shù)中,該函數(shù)由config
對象實(shí)例化:
def start(config: Config):## rest of the code
- 以下幾行創(chuàng)建了引擎和人類玩家的實(shí)例,并在腳本開始運(yùn)行時重置了游戲環(huán)境:
def start(config: Config):...PlayWithHumanConfig().update_play_config(config.play)me_player = Noneenv = ChessEnv().reset()...
- 將創(chuàng)建模型,并使用以下代碼將模型的最佳權(quán)重加載到其中:
def start(config: Config):...model = ChessModel(config)if not load_best_model_weight(model):raise RuntimeError("Best model not found!")player = ChessPlayer(config, model.get_pipes(config.play.search_threads))...
- 前面代碼中的最后一行創(chuàng)建具有指定配置和模型知識的國際象棋引擎玩家實(shí)例:
def start(config: Config):...app = Flask(__name__)@app.route('/play', methods=["GET", "POST"])def play():data = request.get_json()print(data["position"])env.update(data["position"])env.step(data["moves"], False)bestmove = player.action(env, False)return jsonify(bestmove) ...
前面的代碼創(chuàng)建了 Flask 服務(wù)器應(yīng)用的實(shí)例。 定義/play
路由,使其可以接受位置并移動參數(shù),這與我們先前在 UCI 游戲中使用的命令相同。
- 游戲狀態(tài)將更新,并且要求象棋引擎計(jì)算下一個最佳移動。 這以 JSON 格式返回給用戶:
def start(config: Config):...app.run(host="0.0.0.0", port="8080")
腳本的最后一行在主機(jī)0.0.0.0
處啟動 Flask 服務(wù)器,這意味著腳本將監(jiān)聽其運(yùn)行所在設(shè)備的所有打開的 IP。 指定的端口為8080
。
-
最后,我們將腳本部署到我們創(chuàng)建的 VM 實(shí)例。 為此,請執(zhí)行以下步驟:
-
打開 GCP 控制臺的 VM 實(shí)例頁面。
-
輸入在上一節(jié)中創(chuàng)建的 VM 后,單擊
SSH
按鈕。 -
SSH 會話激活后,通過運(yùn)行以下命令來更新系統(tǒng)上的存儲庫:
sudo apt update
- 接下來,使用以下命令克隆存儲庫:
git clone https://github.com/PacktPublishing/Mobile-Deep-Learning-Projects.git
- 將當(dāng)前工作目錄更改為
chess
文件夾,如下所示:
cd Mobile-Deep-Learning-Projects/Chapter8/chess
- 為 Python3 安裝 PIP:
sudo apt install python3-pip
- 安裝項(xiàng)目所需的所有模塊:
pip3 install -r requirements.txt
- 為最初的監(jiān)督學(xué)習(xí)提供訓(xùn)練 PGN。 您可以從這里下載示例 PGN。
ficsgamesdb2017.pgn
文件包含 5,000 個已存儲的游戲。 您需要將此文件上傳到data/play_data/
文件夾。 - 運(yùn)行監(jiān)督學(xué)習(xí)命令:
python3 src/chess_zero/run.py sl
- 運(yùn)行自學(xué)習(xí)命令:
python3 src/chess_zero/run.py self
當(dāng)您對程序可以自行播放的時間感到滿意時,請使用Ctrl + C/Z
停止腳本。
- 運(yùn)行以下命令以啟動服務(wù)器:
python3 src/chess_zero/run.py server
現(xiàn)在,您應(yīng)該能夠?qū)⒙毼缓鸵苿影l(fā)送到服務(wù)器并獲得響應(yīng)。 讓我們快速測試一下。 使用 Postman 或其他任何用于 API 測試的工具,我們將使用 FEN 字符串向 API 發(fā)出請求,以設(shè)置位置和正在進(jìn)行的移動。
假設(shè)您的 VM 實(shí)例正在公共 IP 地址上運(yùn)行(在 VM 實(shí)例儀表板的實(shí)例條目上可見)1.2.3.4。 在這里,我們發(fā)送以下POST
請求:
endpoint: http://1.2.3.4:8080/play
Content-type: JSON
Request body:
{"position": "r1bqk2r/ppp2ppp/2np1n2/2b1p3/2B1P3/2N2N2/PPPPQPPP/R1B1K2R w KQkq - 0 1","moves": "f3g5"
}
先前代碼的輸出為"h7h6"
。 讓我們直觀地了解這種交互。 FEN 中定義的板看起來如下:
我們告訴服務(wù)器這是懷特的舉動,而懷特玩家的舉動是f3g5
,這意味著將懷特騎士移動到板上的 G5 位置。 我們傳遞給 API 的棋盤 FEN 字符串中的'w'
表示白人玩家將進(jìn)行下一回合。
引擎通過將 H7 處的棋子移動到 H6 進(jìn)行響應(yīng),威脅到馬的前進(jìn),如以下屏幕快照所示:
現(xiàn)在,我們可以將此 API 與 Flutter 應(yīng)用集成!
在 Android 上創(chuàng)建簡單的國際象棋 UI
現(xiàn)在,我們了解了強(qiáng)化學(xué)習(xí)以及如何使用它來開發(fā)可部署到 GCP 的國際象棋引擎,讓我們?yōu)橛螒騽?chuàng)建 Flutter 應(yīng)用。 該應(yīng)用將具有兩個播放器–用戶和服務(wù)器。 用戶是玩游戲的人,而服務(wù)器是我們在 GCP 上托管的國際象棋引擎。 首先,用戶采取行動。 記錄此移動并將其以 POST 請求的形式發(fā)送到國際象棋引擎。 然后,國際象棋引擎以自己的動作進(jìn)行響應(yīng),然后在屏幕上進(jìn)行更新。
我們將創(chuàng)建一個簡單的單屏應(yīng)用,將棋盤和棋子放置在中間。 該應(yīng)用將顯示如下:
該應(yīng)用的小部件樹如下所示:
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-LwjjJFDo-1681785128422)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mobi-dl-tflite/img/6f6a925e-3b98-4194-aa42-ee318f257593.png)]
讓我們開始編寫應(yīng)用代碼。
將依賴項(xiàng)添加到pubspec.yaml
首先,將chess_vectors_flutter
包添加到pubspec.yaml
文件中,以便在將要構(gòu)建的棋盤上顯示實(shí)際的棋子。 將以下行添加到pubspec.yaml
的依賴項(xiàng)部分:
chess_vectors_flutter: ">=1.0.6 <2.0.0"
運(yùn)行flutter pub get
安裝包。
將棋子放置在正確的位置可能會有些棘手。 讓我們了解將所有片段放置在正確位置的約定。
了解映射結(jié)構(gòu)
我們將首先創(chuàng)建一個名為chess_game.dart
的新 dart 文件。 這將包含所有游戲邏輯。 在文件內(nèi)部,我們聲明一個名為ChessGame
的有狀態(tài)小部件:
- 要將棋子映射到棋盤的正方形,我們將使用與構(gòu)建模型時相同的符號,以便每個正方形均由字母和數(shù)字表示。 我們將在
ChessGameState
內(nèi)創(chuàng)建一個列表squareList
,以便我們可以存儲所有索引的正方形,如下所示:
var squareList = [ ["a8","b8","c8","d8","e8","f8","g8","h8"],["a7","b7","c7","d7","e7","f7","g7","h7"],["a6","b6","c6","d6","e6","f6","g6","h6"],["a5","b5","c5","d5","e5","f5","g5","h5"],["a4","b4","c4","d4","e4","f4","g4","h4"],["a3","b3","c3","d3","e3","f3","g3","h3"],["a2","b2","c2","d2","e2","f2","g2","h2"],["a1","b1","c1","d1","e1","f1","g1","h1"],];
- 為了將正確的棋子存儲在正確的正方形中并根據(jù)玩家的移動來更新它們,我們將創(chuàng)建一個名為
board
的HashMap
:
HashMap board = new HashMap<String, String>();
HashMap
的鍵將包含正方形的索引,而值將是正方形將保留的片段。 我們將使用一個字符串來表示一塊特定的作品,該字符串將根據(jù)作品的名稱包含一個字母。 例如,K
代表王,B
代表相。 我們通過使用大寫和小寫字母來區(qū)分白色和黑色部分。 大寫字母代表白色,小寫字母代表黑色。 例如,K
代表白王,b
代表黑相。 board['e7'] = "P"
表示索引為'e7'
的盒子當(dāng)前有一個白色棋子。
- 現(xiàn)在,讓我們將它們放置在初始位置。 為此,我們需要定義
initializeBoard()
方法,如下所示:
void initializeBoard() {setState(() {for(int i = 8; i >= 1; i--) {for(int j = 97; j <= 104; j++) {String ch = String.fromCharCode(j)+'$i';board[ch] = " ";}}//Placing White Piecesboard['a1'] = board['h1']= "R";board['b1'] = board['g1'] = "N";board['c1'] = board['f1'] = "B";board['d1'] = "Q";board['e1'] = "K";board['a2'] = board['b2'] = board['c2'] = board['d2'] =board['e2'] = board['f2'] = board['g2'] = board['h2'] = "P";//Placing Black Piecesboard['a8'] = board['h8']= "r";board['b8'] = board['g8'] = "n";board['c8'] = board['f8'] = "b";board['d8'] = "q";board['e8'] = "k";board['a7'] = board['b7'] = board['c7'] = board['d7'] =board['e7'] = board['f7'] = board['g7'] = board['h7'] = "p";});}
在前面的方法中,我們使用一個簡單的嵌套循環(huán)通過從a
到h
的所有行以及從 1 到 8 的所有列進(jìn)行遍歷,使用空白字符串初始化哈希映射板的所有索引。 如“步驟 2”中所述,將其放置在其初始位置上。 為了確保在初始化棋盤時重新繪制 UI,我們將整個分配放在setState()
中。
- 屏幕啟動后,板將被初始化。 為了確保這一點(diǎn),我們需要覆蓋
initState()
并從那里調(diào)用initializeBoard()
:
@overridevoid initState() {super.initState();initializeBoard();}
現(xiàn)在我們對映射棋子有了更好的了解,讓我們開始在屏幕上放置棋子的實(shí)際圖像。
放置實(shí)際片段的圖像
將片段映射到其初始位置后,我們可以開始放置實(shí)際的圖像向量:
- 我們首先定義一個名為
mapImages()
的函數(shù),該函數(shù)采用正方形的索引(即哈希圖板的鍵值)并返回圖像:
Widget mapImages(String squareName) {board.putIfAbsent(squareName, () => " ");String p = board[squareName];var size = 6.0;Widget imageToDisplay = Container();switch (p) {case "P":imageToDisplay = WhitePawn(size: size);break;case "R":imageToDisplay = WhiteRook(size: size);break;case "N":imageToDisplay = WhiteKnight(size: size);break;case "B":imageToDisplay = WhiteBishop(size: size);break;case "Q":imageToDisplay = WhiteQueen(size: size);break;case "K":imageToDisplay = WhiteKing(size: size);break;case "p":imageToDisplay = BlackPawn(size: size);break;case "r":imageToDisplay = BlackRook(size: size);break;case "n":imageToDisplay = BlackKnight(size: size);break;case "b":imageToDisplay = BlackBishop(size: size);break;case "q":imageToDisplay = BlackQueen(size: size);break;case "k":imageToDisplay = BlackKing(size: size);break;case "p":imageToDisplay = BlackPawn(size: size);break;}return imageToDisplay;}
在前面的函數(shù)中,我們構(gòu)建一個與矩形中所含件名相對應(yīng)的開關(guān)盒塊。 我們使用哈希圖在特定的正方形中找到片段,然后返回相應(yīng)的圖像。 例如,如果將a1
的值傳遞到squareName
中,并且哈希圖板具有與鍵值a1
對應(yīng)的值P
,則白兵的圖像將存儲在變量imageToDisplay
中。
請注意,在 64 個棋盤格正方形中,只有 32 個包含棋子。 其余將為空白。 因此,在哈希表board
中,將存在沒有值的鍵。 如果squareName
沒有片段,則將其傳遞給imageToDisplay
變量,該變量將只有一個空容器。
-
在上一步中,我們構(gòu)建了對應(yīng)于棋盤上每個正方形的小部件(圖像或空容器)。 現(xiàn)在,讓我們將所有小部件排列成行和列。
squareName
中的特定元素(例如[a1,b1,....,g1]
)包含應(yīng)并排放置的正方形。 因此,我們將它們包裝成一行并將這些行中的每一個包裝成列。 -
讓我們從定義
buildRow()
方法開始,該方法包含一個列表。 這本質(zhì)上是sqaureName
中的元素列表,并構(gòu)建完整的行。 該方法如下所示:
Widget buildRow(List<String> children) {return Expanded(flex: 1,child: Row(children: children.map((squareName) => getImage(squareName)).toList()),);}
在前面的代碼片段中,我們迭代使用map()
方法傳遞的列表的每個元素。 這會調(diào)用getImage()
以獲取對應(yīng)于正方形的適當(dāng)圖像。 然后,我們將所有這些返回的圖像添加為一行的子級。 該行將一個子代添加到展開的窗口小部件并返回。
getImage()
方法定義如下:
Widget getImage(String squareName) {return Expanded(child: mapImages(squareName),);}
只需輸入squareName
的值,然后返回一個擴(kuò)展的小部件,其中將包含我們先前定義的mapImages
返回的圖像。 我們稍后將修改此方法,以確保玩家可以拖動每個圖像,以便它們可以在棋盤上移動。
- 現(xiàn)在,我們需要構(gòu)建將包含已構(gòu)建行的列。 為此,我們需要定義
buildChessBoard()
方法,如下所示:
Widget buildChessBoard() {return Container(height: 350,child: Column(children: widget.squareList.map((row) {return buildRow(row,);}).toList() ));}
在前面的代碼中,我們迭代了squareList
內(nèi)部的每一行,這些行表示為一個列表。 我們通過調(diào)用buildRow()
來構(gòu)建行,并將它們作為子級添加到列中。 此列作為子級添加到容器中并返回。
- 現(xiàn)在,讓我們將所有片段以及實(shí)際的棋盤圖像放到屏幕上。 我們將覆蓋
build()
方法,以構(gòu)建由棋盤圖像及其碎片組成的小部件棧:
@overrideWidget build(BuildContext context) {return Container(child: Stack(children: <Widget>[Container(child: new Center(child: Image.asset("assets/chess_board.png", fit: BoxFit.cover,)),),Center(child: Container(child: buildChessBoard(),),)],));}
前面的方法使用容器來構(gòu)建棧,該容器添加存儲在assets
文件夾中的棋盤圖像。 棧的下一個子項(xiàng)是居中對齊的容器,其中所有片段圖像都通過對buildChessBoard()
的調(diào)用以小部件的形式添加為行和列包裝。 整個棧作為子級添加到容器中并返回,以便出現(xiàn)在屏幕上。
此時,應(yīng)用顯示棋盤,以及所有放置在其初始位置的棋子。 如下所示:
現(xiàn)在,讓我們使這些棋子變得可移動,以便我們可以玩一個真實(shí)的游戲。
使片段移動
在本節(jié)中,我們將用可拖動的工具包裝每塊棋子,以便用戶能夠?qū)⑵遄油蟿拥剿栉恢谩?讓我們詳細(xì)看一下實(shí)現(xiàn):
- 回想一下,我們聲明了一個哈希圖來存儲片段的位置。 移動將包括從一個盒子中移出一塊并將其放在另一個盒子中。 假設(shè)我們有兩個變量
'from'
和'to'
,它們存儲用于移動片段的盒子的索引。 進(jìn)行移動后,我們拿起'from'
處的片段并將其放入'to'
中。 因此,'from'
的框變?yōu)榭铡?按照相同的邏輯,我們將定義refreshBoard()
方法,該方法在每次移動時都會調(diào)用:
void refreshBoard(String from, String to) {setState(() {board[to] = board[from];board[from] = " ";});}
from
和to
變量存儲源和目標(biāo)正方形的索引。 這些值在board
HasMhap 中用作鍵。 進(jìn)行移動時,from
處的棋子會移至to.
。此后,from
處的方塊應(yīng)該變空。 它包含在setState()
中,以確保每次移動后都更新 UI。
- 現(xiàn)在,讓我們將其拖曳。 為此,我們將拖動項(xiàng)附加到
getPieceImage()
方法返回的木板的每個圖像小部件上。 我們通過修改方法來做到這一點(diǎn):
Widget getImage(String squareName) {return Expanded(child: DragTarget<List>(builder: (context, accepted, rejected) {return Draggable<List>(child: mapImages(squareName),feedback: mapImages(squareName),onDragCompleted: () {},data: [squareName,],);}, onWillAccept: (willAccept) {return true;}, onAccept: (List moveInfo) {String from = moveInfo[0];String to = squareName;refreshBoard(from, to);}));}
在前面的函數(shù)中,我們首先將特定正方形的圖像包裝在Draggable
中。 此類用于感測和跟隨屏幕上的拖動手勢。 child
屬性用于指定要拖動的窗口小部件,而反饋內(nèi)部的窗口小部件用于跟蹤手指在屏幕上的移動。 當(dāng)拖動完成并且用戶抬起手指時,目標(biāo)將有機(jī)會接受所攜帶的數(shù)據(jù)。 由于我們正在源和目標(biāo)之間移動,因此我們將添加Draggable
作為DragTarget
的子代,以便可以在源和目標(biāo)之間移動小部件。 onWillAccept
設(shè)置為true
,以便可以進(jìn)行所有移動。
可以修改此屬性,以使其具有可以區(qū)分合法象棋動作并且不允許拖動非法動作的功能。 放下片段并完成拖動后,將調(diào)用onAccept
。 moveInfo
列表保存有關(guān)拖動源的信息。 在這里,我們調(diào)用refreshBoard()
,并傳入from
和to
的值,以便屏幕可以反映運(yùn)動。 至此,我們完成了向用戶顯示初始棋盤的操作,并使棋子可以在盒子之間移動。
在下一節(jié)中,我們將通過對托管的國際象棋服務(wù)器進(jìn)行 API 調(diào)用來增加應(yīng)用的交互性。 這些將使游戲栩栩如生。
將國際象棋引擎 API 與 UI 集成
托管的棋牌服務(wù)器將作為對手玩家添加到應(yīng)用中。 用戶將是白色的一面,而服務(wù)器將是黑色的一面。 這里要實(shí)現(xiàn)的游戲邏輯非常簡單。 第一步是提供給應(yīng)用用戶。 用戶進(jìn)行移動時,他們將棋盤的狀態(tài)從狀態(tài) X 更改為狀態(tài) Y。棋盤的狀態(tài)由 FEN 字符串表示。 同樣,他們將一塊from
移到一個特定的正方形to
移到一個特定的正方形,這有助于他們的移動。 當(dāng)用戶完成移動時,狀態(tài) X 的 FEN 字符串及其當(dāng)前移動(通過將from
和to
正方形連接在一起而獲得)以POST
請求的形式發(fā)送到服務(wù)器。 作為回報,服務(wù)器從其側(cè)面進(jìn)行下一步移動,然后將其反映在 UI 上。
讓我們看一下此邏輯的代碼:
- 首先,我們定義一個名為
getPositionString()
的方法來為應(yīng)用的特定狀態(tài)生成 FEN 字符串:
String getPositionString(String move) {String s = "";for(int i = 8; i >= 1; i--) {int count = 0;for(int j = 97; j <= 104; j++) {String ch = String.fromCharCode(j)+'$i';if(board[ch] == " ") {count += 1;if(j == 104) s = s + "$count";} else {if(count > 0) s = s + "$count";s = s + board[ch];count = 0;}}s = s + "/";}String position = s.substring(0, s.length-1) + " w KQkq - 0 1";var json = jsonEncode({"position": position, "moves": move});
}
在前面的方法中,我們將move
作為參數(shù),它是from
和to
變量的連接。 接下來,我們?yōu)槠灞P的當(dāng)前狀態(tài)創(chuàng)建 FEN 字符串。 創(chuàng)建 FEN 字符串背后的邏輯是,我們遍歷電路板的每一行并為該行創(chuàng)建一個字符串。 然后將生成的字符串連接到最終字符串。
讓我們借助示例更好地理解這一點(diǎn)。 考慮一個rnbqkbnr/pp1ppppp/8/1p6/8/3P4/PPP1PPPP/RNBQKBNR w KQkq - 0 1
的 FEN 字符串。 在此,每行可以用八個或更少的字符表示。 特定行的狀態(tài)通過使用分隔符“/”與另一行分開。 對于特定的行,每件作品均以其指定的符號表示,其中P
表示白兵,b
表示黑相。 每個占用的正方形均由件符號明確表示。 例如,PpkB
指示板上的前四個正方形被白色棋子,黑色棋子,黑色國王和白色主教占據(jù)。 對于空盒子,使用整數(shù),該數(shù)字表示可傳染的空盒子的數(shù)量。 注意示例 FEN 字符串中的8
。 這表示該行的所有 8 個正方形均為空。 3P4
表示前三個正方形為空,第四個方框被白色棋子占據(jù),并且四個正方形為空。
在getPositionString()
方法中,我們迭代從 8 到 1 的每一行,并為每行生成一個狀態(tài)字符串。 對于每個非空框,我們只需在's'
變量中添加一個表示該塊的字符。 對于每個空框,當(dāng)找到非空框或到達(dá)行末時,我們將count
的值增加 1 并將其連接到's'
字符串。 遍歷每一行后,我們添加“/”以分隔兩行。 最后,我們通過將生成的's'
字符串與w KQkq - 0 1
連接來生成位置字符串。 然后,我們通過將jsonEncode()
與鍵值對結(jié)合使用來生成所需的 JSON 對象
- 我們使用“步驟 1”的“步驟 1”中的
from
和to
變量來保存用戶的當(dāng)前移動。 我們可以通過在refreshBoard()
方法中添加兩行來實(shí)現(xiàn):
void refreshBoard(String from, String to) {String move= from + to;getPositionString(move);.....
}
在前面的代碼片段中,我們將from
和to
的值連接起來,并將它們存儲在名為move
的字符串變量中。 然后,我們調(diào)用getPositionString()
,并將move
的值傳遞給參數(shù)。
- 接下來,我們使用在上一步中
makePOSTRequest()
方法中生成的JSON
向服務(wù)器發(fā)出POST
請求:
void makePOSTRequest(var json) async{var url = 'http://35.200.253.0:8080/play';var response = await http.post(url, headers: {"Content-Type": "application/json"} ,body: json);String rsp = response.body;String from = rsp.substring(0,3);String to = rsp.substring(3);
}
首先,將國際象棋服務(wù)器的 IP 地址存儲在url
變量中。 然后,我們使用http.post()
發(fā)出HTTP POST
請求,并為 URL,標(biāo)頭和正文傳遞正確的值。 POST 請求的響應(yīng)包含服務(wù)器端的下一個動作,并存儲在變量響應(yīng)中。 我們解析響應(yīng)的主體并將其存儲在名為rsp
的字符串變量中。 響應(yīng)基本上是一個字符串,是服務(wù)器端的源方和目標(biāo)方的連接。 例如,響應(yīng)字符串f4a3
表示國際象棋引擎希望將棋子以f4
正方形移動到a3
正方形。 我們使用substring()
分隔源和目標(biāo),并將值存儲在from
和to
變量中。
- 現(xiàn)在,通過將調(diào)用添加到
makePOSTrequest()
來從getPositionString()
發(fā)出 POST 請求:
String getPositionString(String move) {.....makePOSTRequest(json);
}
在 FEN 字符串生成板的給定狀態(tài)之后,對makePOSTrequest()
的調(diào)用添加在函數(shù)的最后。
- 最后,我們使用
refreshBoardFromServer()
方法刷新板以反映服務(wù)器在板上的移動:
void refreshBoardFromServer(String from, String to) {setState(() { board[to] = board[from];board[from] = " ";});
}
前述方法中的邏輯非常簡單。 首先,我們將映射到from
索引正方形的片段移動到to
索引正方形,然后清空from
索引正方形。
- 最后,我們調(diào)用適當(dāng)?shù)姆椒ㄒ杂米钚碌膭幼鞲?UI:
void makePOSTRequest(var json) async{......refreshBoardFromServer(from, to);buildChessBoard();
}
發(fā)布請求成功完成后,我們收到了服務(wù)器的響應(yīng),我們將調(diào)用refreshBoardFromServer()
以更新板上的映射。 最后,我們調(diào)用buildChessBoard()
以在應(yīng)用屏幕上反映國際象棋引擎所做的最新動作。
以下屏幕快照顯示了國際象棋引擎進(jìn)行移動后的更新的用戶界面:
請注意,黑色的塊在白色的塊之后移動。 這就是代碼的工作方式。 首先,用戶采取行動。 它以板的初始狀態(tài)發(fā)送到服務(wù)器。 然后,服務(wù)器以其移動進(jìn)行響應(yīng),更新 UI。 作為練習(xí),您可以嘗試實(shí)現(xiàn)一些邏輯以區(qū)分有效動作和無效動作。
可以在這個頁面中找到此代碼。
現(xiàn)在,讓我們通過創(chuàng)建材質(zhì)應(yīng)用來包裝應(yīng)用。
創(chuàng)建材質(zhì)應(yīng)用
現(xiàn)在,我們將在main.dart
中創(chuàng)建最終的材質(zhì)應(yīng)用。 讓我們從以下步驟開始:
- 首先,我們創(chuàng)建無狀態(tài)窗口小部件
MyApp
,并覆蓋其build()
方法,如下所示:
class MyApp extends StatelessWidget {@overrideWidget build(BuildContext context) {return MaterialApp(title: 'Chess',theme: ThemeData(primarySwatch: Colors.blue,),home: MyHomePage(title: 'Chess'),);}
}
- 我們創(chuàng)建一個單獨(dú)的
StatefulWidget
,稱為MyHomePage
,以便將 UI 放置在屏幕中央。MyHomePage
的build()
方法如下所示:
@override
Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text('Chess'),),body: Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: <Widget>[ChessGame()],),),);
}
- 最后,我們通過在
main.dart
中添加以下行來執(zhí)行整個代碼:
void main() => runApp(MyApp());
而已! 現(xiàn)在,我們有一個交互式的國際象棋游戲應(yīng)用,您可以與聰明的對手一起玩。 希望你贏!
整個文件的代碼可以在這個頁面中找到。
總結(jié)
在此項(xiàng)目中,我們介紹了強(qiáng)化學(xué)習(xí)的概念以及為什么強(qiáng)化學(xué)習(xí)在創(chuàng)建游戲性 AI 的開發(fā)人員中很受歡迎。 我們討論了 Google DeepMind 的 AlphaGo 及其兄弟項(xiàng)目,并深入研究了它們的工作算法。 接下來,我們創(chuàng)建了一個類似的程序來玩 Connect 4,然后下棋。 我們將基于 AI 的國際象棋引擎作為 API 部署到 GPU 實(shí)例的 GCP 上,并將其與基于 Flutter 的應(yīng)用集成。 我們還了解了如何使用 UCI 促進(jìn)國際象棋的無狀態(tài)游戲。 完成此項(xiàng)目后,您將對如何將游戲轉(zhuǎn)換為強(qiáng)化學(xué)習(xí)環(huán)境,如何以編程方式定義游戲規(guī)則以及如何創(chuàng)建用于玩這些游戲的自學(xué)智能體有很好的了解。
在下一章中,我們將創(chuàng)建一個應(yīng)用,該應(yīng)用可以使低分辨率圖像變成非常高分辨率的圖像。 我們將在 AI 的幫助下進(jìn)行此操作。
八、深度神經(jīng)網(wǎng)絡(luò)
在本章中,我們將回顧機(jī)器學(xué)習(xí),深度神經(jīng)網(wǎng)絡(luò)中最先進(jìn)的技術(shù),也是研究最多的領(lǐng)域之一。
深度神經(jīng)網(wǎng)絡(luò)定義
這是一個新聞技術(shù)領(lǐng)域蓬勃發(fā)展的領(lǐng)域,每天我們都聽到成功地將 DNN 用于解決新問題的實(shí)驗(yàn),例如計(jì)算機(jī)視覺,自動駕駛,語音和文本理解等。
在前幾章中,我們使用了與 DNN 相關(guān)的技術(shù),尤其是在涉及卷積神經(jīng)網(wǎng)絡(luò)的技術(shù)中。
出于實(shí)際原因,我們將指深度學(xué)習(xí)和深度神經(jīng)網(wǎng)絡(luò),即其中層數(shù)明顯優(yōu)于幾個相似層的架構(gòu),我們將指代具有數(shù)十個層的神經(jīng)網(wǎng)絡(luò)架構(gòu),或者復(fù)雜結(jié)構(gòu)的組合。
穿越時空的深度網(wǎng)絡(luò)架構(gòu)
在本節(jié)中,我們將回顧從 LeNet5 開始在整個深度學(xué)習(xí)歷史中出現(xiàn)的里程碑架構(gòu)。
LeNet 5
在 1980 年代和 1990 年代,神經(jīng)網(wǎng)絡(luò)領(lǐng)域一直保持沉默。 盡管付出了一些努力,但是架構(gòu)非常簡單,并且需要大的(通常是不可用的)機(jī)器力量來嘗試更復(fù)雜的方法。
1998 年左右,在貝爾實(shí)驗(yàn)室中,在圍繞手寫校驗(yàn)數(shù)字分類的研究中,Ian LeCun 開始了一種新趨勢,該趨勢實(shí)現(xiàn)了所謂的“深度學(xué)習(xí)——卷積神經(jīng)網(wǎng)絡(luò)”的基礎(chǔ),我們已經(jīng)在第 5 章,簡單的前饋神經(jīng)網(wǎng)絡(luò)中對其進(jìn)行了研究。
在那些年里,SVM 和其他更嚴(yán)格定義的技術(shù)被用來解決這類問題,但是有關(guān) CNN 的基礎(chǔ)論文表明,與當(dāng)時的現(xiàn)有方法相比,神經(jīng)網(wǎng)絡(luò)的表現(xiàn)可以與之媲美或更好。
Alexnet
經(jīng)過幾年的中斷(即使 LeCun 繼續(xù)將其網(wǎng)絡(luò)應(yīng)用到其他任務(wù),例如人臉和物體識別),可用結(jié)構(gòu)化數(shù)據(jù)和原始處理能力的指數(shù)增長,使團(tuán)隊(duì)得以增長和調(diào)整模型, 在某種程度上被認(rèn)為是不可能的,因此可以增加模型的復(fù)雜性,而無需等待數(shù)月的訓(xùn)練。
來自許多技術(shù)公司和大學(xué)的計(jì)算機(jī)研究團(tuán)隊(duì)開始競爭一些非常艱巨的任務(wù),包括圖像識別。 對于以下挑戰(zhàn)之一,即 Imagenet 分類挑戰(zhàn),開發(fā)了 Alexnet 架構(gòu):
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-lULpcW1A-1681785128423)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mobi-dl-tflite/img/00125.jpg)]
Alexnet 架構(gòu)
主要功能
從其第一層具有卷積運(yùn)算的意義上講,Alexnet 可以看作是增強(qiáng)的 LeNet5。 但要添加未使用過的最大池化層,然后添加一系列密集的連接層,以建立最后的輸出類別概率層。 視覺幾何組(VGG)模型
圖像分類挑戰(zhàn)的其他主要競爭者之一是牛津大學(xué)的 VGG。
VGG 網(wǎng)絡(luò)架構(gòu)的主要特征是它們將卷積濾波器的大小減小到一個簡單的3x3
,并按順序組合它們。
微小的卷積內(nèi)核的想法破壞了 LeNet 及其后繼者 Alexnet 的最初想法,后者最初使用的過濾器高達(dá)11x11
過濾器,但復(fù)雜得多且表現(xiàn)低下。 過濾器大小的這種變化是當(dāng)前趨勢的開始:
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-r8DheOZh-1681785128423)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mobi-dl-tflite/img/00126.jpg)]
VGG 中每層的參數(shù)編號摘要
然而,使用一系列小的卷積權(quán)重的積極變化,總的設(shè)置是相當(dāng)數(shù)量的參數(shù)(數(shù)以百萬計(jì)的數(shù)量級),因此它必須受到許多措施的限制。
原始的初始模型
在由 Alexnet 和 VGG 主導(dǎo)的兩個主要研究周期之后,Google 憑借非常強(qiáng)大的架構(gòu) Inception 打破了挑戰(zhàn),該架構(gòu)具有多次迭代。
這些迭代的第一個迭代是從其自己的基于卷積神經(jīng)網(wǎng)絡(luò)層的架構(gòu)版本(稱為 GoogLeNet)開始的,該架構(gòu)的名稱讓人想起了始于網(wǎng)絡(luò)的方法。
GoogLenet(InceptionV1)
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-ofFPZuno-1681785128424)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mobi-dl-tflite/img/00127.jpg)]
InceptionV1
GoogLeNet 是這項(xiàng)工作的第一個迭代,如下圖所示,它具有非常深的架構(gòu),但是它具有九個鏈?zhǔn)匠跏寄K的令人毛骨悚然的總和,幾乎沒有或根本沒有修改:
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-EPjLvndu-1681785128424)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mobi-dl-tflite/img/00128.jpg)]
盜夢空間原始架構(gòu)
與兩年前發(fā)布的 Alexnet 相比,它是如此復(fù)雜,但它設(shè)法減少了所需的參數(shù)數(shù)量并提高了準(zhǔn)確率。
但是,由于幾乎所有結(jié)構(gòu)都由相同原始結(jié)構(gòu)層構(gòu)建塊的確定排列和重復(fù)組成,因此提高了此復(fù)雜架構(gòu)的理解和可伸縮性。
批量歸一化初始化(V2)
2015 年最先進(jìn)的神經(jīng)網(wǎng)絡(luò)在提高迭代效率的同時,還存在訓(xùn)練不穩(wěn)定的問題。
為了理解問題的構(gòu)成,首先我們將記住在前面的示例中應(yīng)用的簡單正則化步驟。 它主要包括將這些值以零為中心,然后除以最大值或標(biāo)準(zhǔn)偏差,以便為反向傳播的梯度提供良好的基線。
在訓(xùn)練非常大的數(shù)據(jù)集的過程中,發(fā)生的事情是,經(jīng)過大量訓(xùn)練示例之后,不同的值振蕩開始放大平均參數(shù)值,就像在共振現(xiàn)象中一樣。 我們非常簡單地描述的被稱為協(xié)方差平移。
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-sz6uJZfR-1681785128424)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mobi-dl-tflite/img/00129.jpg)]
有和沒有批量歸一化的表現(xiàn)比較
這是開發(fā)批歸一化技術(shù)的主要原因。
再次簡化了過程描述,它不僅包括對原始輸入值進(jìn)行歸一化,還對每一層上的輸出值進(jìn)行了歸一化,避免了在層之間出現(xiàn)不穩(wěn)定性之前就開始影響或漂移這些值。
這是 Google 在 2015 年 2 月發(fā)布的改進(jìn)版 GoogLeNet 實(shí)現(xiàn)中提供的主要功能,也稱為 InceptionV2。
InceptionV3
快進(jìn)到 2015 年 12 月,Inception 架構(gòu)有了新的迭代。 兩次發(fā)行之間月份的不同使我們對新迭代的開發(fā)速度有了一個想法。
此架構(gòu)的基本修改如下:
- 將卷積數(shù)減少到最大
3x3
- 增加網(wǎng)絡(luò)的總體深度
- 在每一層使用寬度擴(kuò)展技術(shù)來改善特征組合
下圖說明了如何解釋改進(jìn)的啟動模塊:
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-6JVkvOHu-1681785128424)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mobi-dl-tflite/img/00130.jpg)]
InceptionV3 基本模塊
這是整個 V3 架構(gòu)的表示形式,其中包含通用構(gòu)建模塊的許多實(shí)例:
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-doHC5UCK-1681785128424)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mobi-dl-tflite/img/00131.jpg)]
InceptionV3 總體圖
殘差網(wǎng)絡(luò)(ResNet)
殘差網(wǎng)絡(luò)架構(gòu)于 2015 年 12 月出現(xiàn)(與 InceptionV3 幾乎同時出現(xiàn)),它帶來了一個簡單而新穎的想法:不僅使用每個構(gòu)成層的輸出,還將該層的輸出與原始輸入結(jié)合。
在下圖中,我們觀察到 ResNet 模塊之一的簡化??視圖。 它清楚地顯示了卷積層棧末尾的求和運(yùn)算,以及最終的 relu 運(yùn)算:
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-lrxuW1RM-1681785128425)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mobi-dl-tflite/img/00132.jpg)]
ResNet 一般架構(gòu)
模塊的卷積部分包括將特征從 256 個值減少到 64 個值,一個保留特征數(shù)的3x3
過濾層以及一個從 64 x 256 個值增加1x1
層的特征。 在最近的發(fā)展中,ResNet 的使用深度還不到 30 層,分布廣泛。
其他深度神經(jīng)網(wǎng)絡(luò)架構(gòu)
最近開發(fā)了很多神經(jīng)網(wǎng)絡(luò)架構(gòu)。 實(shí)際上,這個領(lǐng)域是如此活躍,以至于我們每年或多或少都有新的杰出架構(gòu)外觀。 最有前途的神經(jīng)網(wǎng)絡(luò)架構(gòu)的列表是:
- SqueezeNet:此架構(gòu)旨在減少 Alexnet 的參數(shù)數(shù)量和復(fù)雜性,聲稱減少了 50 倍的參數(shù)數(shù)量
- 高效神經(jīng)網(wǎng)絡(luò)(Enet):旨在構(gòu)建更簡單,低延遲的浮點(diǎn)運(yùn)算數(shù)量,具有實(shí)時結(jié)果的神經(jīng)網(wǎng)絡(luò)
- Fractalnet:它的主要特征是非常深的網(wǎng)絡(luò)的實(shí)現(xiàn),不需要?dú)埩舻募軜?gòu),將結(jié)構(gòu)布局組織為截斷的分形
示例 – 風(fēng)格繪畫 – VGG 風(fēng)格遷移
在此示例中,我們將配合 Leon Gatys 的論文《藝術(shù)風(fēng)格的神經(jīng)算法》的實(shí)現(xiàn)。
注意
此練習(xí)的原始代碼由 Anish Athalye 提供。
我們必須注意,此練習(xí)沒有訓(xùn)練內(nèi)容。 我們將僅加載由 VLFeat 提供的預(yù)訓(xùn)練系數(shù)矩陣,該矩陣是預(yù)訓(xùn)練模型的數(shù)據(jù)庫,可用于處理模型,從而避免了通常需要大量計(jì)算的訓(xùn)練:
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-tTvYAxhb-1681785128425)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mobi-dl-tflite/img/00133.jpg)]
風(fēng)格遷移主要概念
有用的庫和方法
- 使用
scipy.io.loadmat
加載參數(shù)文件- 我們將使用的第一個有用的庫是
scipy.io
模塊,用于加載系數(shù)數(shù)據(jù),該數(shù)據(jù)另存為 matlab 的 MAT 格式。
- 我們將使用的第一個有用的庫是
- 上一個參數(shù)的用法:
scipy.io.loadmat(file_name, mdict=None, appendmat=True, **kwargs)
-
返回前一個參數(shù):
mat_dict : dict :dictionary
,變量名作為鍵,加載的矩陣作為值。 如果填充了mdict
參數(shù),則將結(jié)果分配給它。
數(shù)據(jù)集說明和加載
為了解決這個問題,我們將使用預(yù)訓(xùn)練的數(shù)據(jù)集,即 VGG 神經(jīng)網(wǎng)絡(luò)的再訓(xùn)練系數(shù)和 Imagenet 數(shù)據(jù)集。
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-ht3GTlIo-1681785128425)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mobi-dl-tflite/img/00134.jpg)]
數(shù)據(jù)集預(yù)處理
假設(shè)系數(shù)是在加載的參數(shù)矩陣中給出的,那么關(guān)于初始數(shù)據(jù)集的工作就不多了。
模型架構(gòu)
模型架構(gòu)主要分為兩部分:風(fēng)格和內(nèi)容。
為了生成最終圖像,使用了沒有最終完全連接層的 VGG 網(wǎng)絡(luò)。
損失函數(shù)
該架構(gòu)定義了兩個不同的損失函數(shù)來優(yōu)化最終圖像的兩個不同方面,一個用于內(nèi)容,另一個用于風(fēng)格。
內(nèi)容損失函數(shù)
content_loss
函數(shù)的代碼如下:
# content loss content_loss = content_weight * (2 * tf.nn.l2_loss( net[CONTENT_LAYER] - content_features[CONTENT_LAYER]) / content_features[CONTENT_LAYER].size)
風(fēng)格損失函數(shù)
損失優(yōu)化循環(huán)
損耗優(yōu)化循環(huán)的代碼如下:
best_loss = float('inf') best = None with tf.Session() as sess: sess.run(tf.initialize_all_variables()) for i in range(iterations): last_step = (i == iterations - 1) print_progress(i, last=last_step) train_step.run() if (checkpoint_iterations and i % checkpoint_iterations == 0) or last_step: this_loss = loss.eval() if this_loss < best_loss: best_loss = this_loss best = image.eval() yield ( (None if last_step else i), vgg.unprocess(best.reshape(shape[1:]), mean_pixel) )
收斂性測試
在此示例中,我們將僅檢查指示的迭代次數(shù)(迭代參數(shù))。
程序執(zhí)行
為了以良好的迭代次數(shù)(大約 1000 個)執(zhí)行該程序,我們建議至少有 8GB 的 RAM 內(nèi)存可用:
python neural_style.py --content examples/2-content.jpg --styles examples/2-style1.jpg --checkpoint-iterations=100 --iterations=1000 --checkpoint-output=out%s.jpg --output=outfinal
前面命令的結(jié)果如下:
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-IISmOKsl-1681785128425)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mobi-dl-tflite/img/00135.jpg)]
風(fēng)格遷移步驟
控制臺輸出如下:
Iteration 1/1000
Iteration 2/1000
Iteration 3/1000
Iteration 4/1000
...
Iteration 999/1000
Iteration 1000/1000content loss: 908786style loss: 261789tv loss: 25639.9total loss: 1.19621e+06
完整源代碼
neural_style.py
的代碼如下:
import os import numpy as np
import scipy.misc from stylize import stylize import math
from argparse import ArgumentParser # default arguments
CONTENT_WEIGHT = 5e0
STYLE_WEIGHT = 1e2
TV_WEIGHT = 1e2
LEARNING_RATE = 1e1
STYLE_SCALE = 1.0
ITERATIONS = 100
VGG_PATH = 'imagenet-vgg-verydeep-19.mat' def build_parser(): parser = ArgumentParser() parser.add_argument('--content', dest='content', help='content image', metavar='CONTENT', required=True) parser.add_argument('--styles', dest='styles', nargs='+', help='one or more style images', metavar='STYLE', required=True) parser.add_argument('--output', dest='output', help='output path', metavar='OUTPUT', required=True) parser.add_argument('--checkpoint-output', dest='checkpoint_output', help='checkpoint output format', metavar='OUTPUT') parser.add_argument('--iterations', type=int, dest='iterations', help='iterations (default %(default)s)', metavar='ITERATIONS', default=ITERATIONS) parser.add_argument('--width', type=int, dest='width', help='output width', metavar='WIDTH') parser.add_argument('--style-scales', type=float, dest='style_scales', nargs='+', help='one or more style scales', metavar='STYLE_SCALE') parser.add_argument('--network', dest='network', help='path to network parameters (default %(default)s)', metavar='VGG_PATH', default=VGG_PATH) parser.add_argument('--content-weight', type=float, dest='content_weight', help='content weight (default %(default)s)', metavar='CONTENT_WEIGHT', default=CONTENT_WEIGHT) parser.add_argument('--style-weight', type=float, dest='style_weight', help='style weight (default %(default)s)', metavar='STYLE_WEIGHT', default=STYLE_WEIGHT) parser.add_argument('--style-blend-weights', type=float, dest='style_blend_weights', help='style blending weights', nargs='+', metavar='STYLE_BLEND_WEIGHT') parser.add_argument('--tv-weight', type=float, dest='tv_weight', help='total variation regularization weight (default %(default)s)', metavar='TV_WEIGHT', default=TV_WEIGHT) parser.add_argument('--learning-rate', type=float, dest='learning_rate', help='learning rate (default %(default)s)', metavar='LEARNING_RATE', default=LEARNING_RATE) parser.add_argument('--initial', dest='initial', help='initial image', metavar='INITIAL') parser.add_argument('--print-iterations', type=int, dest='print_iterations', help='statistics printing frequency', metavar='PRINT_ITERATIONS') parser.add_argument('--checkpoint-iterations', type=int, dest='checkpoint_iterations', help='checkpoint frequency', metavar='CHECKPOINT_ITERATIONS') return parser def main(): parser = build_parser() options = parser.parse_args() if not os.path.isfile(options.network): parser.error("Network %s does not exist. (Did you forget to download it?)" % options.network) content_image = imread(options.content) style_images = [imread(style) for style in options.styles] width = options.width if width is not None: new_shape = (int(math.floor(float(content_image.shape[0]) / content_image.shape[1] * width)), width) content_image = scipy.misc.imresize(content_image, new_shape) target_shape = content_image.shape for i in range(len(style_images)): style_scale = STYLE_SCALE if options.style_scales is not None: style_scale = options.style_scales[i] style_images[i] = scipy.misc.imresize(style_images[i], style_scale * target_shape[1] / style_images[i].shape[1]) style_blend_weights = options.style_blend_weights if style_blend_weights is None: # default is equal weights style_blend_weights = [1.0/len(style_images) for _ in style_images] else: total_blend_weight = sum(style_blend_weights) style_blend_weights = [weight/total_blend_weight for weight in style_blend_weights] initial = options.initial if initial is not None: initial = scipy.misc.imresize(imread(initial), content_image.shape[:2]) if options.checkpoint_output and "%s" not in options.checkpoint_output: parser.error("To save intermediate images, the checkpoint output " "parameter must contain `%s` (e.g. `foo%s.jpg`)") for iteration, image in stylize( network=options.network, initial=initial, content=content_image, styles=style_images, iterations=options.iterations, content_weight=options.content_weight, style_weight=options.style_weight, style_blend_weights=style_blend_weights, tv_weight=options.tv_weight, learning_rate=options.learning_rate, print_iterations=options.print_iterations, checkpoint_iterations=options.checkpoint_iterations ): output_file = None if iteration is not None: if options.checkpoint_output: output_file = options.checkpoint_output % iteration else: output_file = options.output if output_file: imsave(output_file, image) def imread(path): return scipy.misc.imread(path).astype(np.float) def imsave(path, img): img = np.clip(img, 0, 255).astype(np.uint8) scipy.misc.imsave(path, img) if __name__ == '__main__': main()
Stilize.py
的代碼如下:
import vgg import tensorflow as tf
import numpy as np from sys import stderr CONTENT_LAYER = 'relu4_2'
STYLE_LAYERS = ('relu1_1', 'relu2_1', 'relu3_1', 'relu4_1', 'relu5_1') try: reduce
except NameError: from functools import reduce def stylize(network, initial, content, styles, iterations, content_weight, style_weight, style_blend_weights, tv_weight, learning_rate, print_iterations=None, checkpoint_iterations=None): """ Stylize images. This function yields tuples (iteration, image); `iteration` is None if this is the final image (the last iteration). Other tuples are yielded every `checkpoint_iterations` iterations. :rtype: iterator[tuple[int|None,image]] """ shape = (1,) + content.shape style_shapes = [(1,) + style.shape for style in styles] content_features = {} style_features = [{} for _ in styles] # compute content features in feedforward mode g = tf.Graph() with g.as_default(), g.device('/cpu:0'), tf.Session() as sess: image = tf.placeholder('float', shape=shape) net, mean_pixel = vgg.net(network, image) content_pre = np.array([vgg.preprocess(content, mean_pixel)]) content_features[CONTENT_LAYER] = net[CONTENT_LAYER].eval( feed_dict={image: content_pre}) # compute style features in feedforward mode for i in range(len(styles)): g = tf.Graph() with g.as_default(), g.device('/cpu:0'), tf.Session() as sess: image = tf.placeholder('float', shape=style_shapes[i]) net, _ = vgg.net(network, image) style_pre = np.array([vgg.preprocess(styles[i], mean_pixel)]) for layer in STYLE_LAYERS: features = net[layer].eval(feed_dict={image: style_pre}) features = np.reshape(features, (-1, features.shape[3])) gram = np.matmul(features.T, features) / features.size style_features[i][layer] = gram # make stylized image using backpropogation with tf.Graph().as_default(): if initial is None: noise = np.random.normal(size=shape, scale=np.std(content) * 0.1) initial = tf.random_normal(shape) * 0.256 else: initial = np.array([vgg.preprocess(initial, mean_pixel)]) initial = initial.astype('float32') image = tf.Variable(initial) net, _ = vgg.net(network, image) # content loss content_loss = content_weight * (2 * tf.nn.l2_loss( net[CONTENT_LAYER] - content_features[CONTENT_LAYER]) / content_features[CONTENT_LAYER].size) # style loss style_loss = 0 for i in range(len(styles)): style_losses = [] for style_layer in STYLE_LAYERS: layer = net[style_layer] _, height, width, number = map(lambda i: i.value, layer.get_shape()) size = height * width * number feats = tf.reshape(layer, (-1, number)) gram = tf.matmul(tf.transpose(feats), feats) / size style_gram = style_features[i][style_layer] style_losses.append(2 * tf.nn.l2_loss(gram - style_gram) / style_gram.size) style_loss += style_weight * style_blend_weights[i] * reduce(tf.add, style_losses) # total variation denoising tv_y_size = _tensor_size(image[:,1:,:,:]) tv_x_size = _tensor_size(image[:,:,1:,:]) tv_loss = tv_weight * 2 * ( (tf.nn.l2_loss(image[:,1:,:,:] - image[:,:shape[1]-1,:,:]) / tv_y_size) + (tf.nn.l2_loss(image[:,:,1:,:] - image[:,:,:shape[2]-1,:]) / tv_x_size)) # overall loss loss = content_loss + style_loss + tv_loss # optimizer setup train_step = tf.train.AdamOptimizer(learning_rate).minimize(loss) def print_progress(i, last=False): stderr.write('Iteration %d/%d\n' % (i + 1, iterations)) if last or (print_iterations and i % print_iterations == 0): stderr.write(' content loss: %g\n' % content_loss.eval()) stderr.write(' style loss: %g\n' % style_loss.eval()) stderr.write(' tv loss: %g\n' % tv_loss.eval()) stderr.write(' total loss: %g\n' % loss.eval()) # optimization best_loss = float('inf') best = None with tf.Session() as sess: sess.run(tf.initialize_all_variables()) for i in range(iterations): last_step = (i == iterations - 1) print_progress(i, last=last_step) train_step.run() if (checkpoint_iterations and i % checkpoint_iterations == 0) or last_step: this_loss = loss.eval() if this_loss < best_loss: best_loss = this_loss best = image.eval() yield ( (None if last_step else i), vgg.unprocess(best.reshape(shape[1:]), mean_pixel) ) def _tensor_size(tensor): from operator import mul return reduce(mul, (d.value for d in tensor.get_shape()), 1) vgg.py
import tensorflow as tf
import numpy as np
import scipy.io def net(data_path, input_image): layers = ( 'conv1_1', 'relu1_1', 'conv1_2', 'relu1_2', 'pool1', 'conv2_1', 'relu2_1', 'conv2_2', 'relu2_2', 'pool2', 'conv3_1', 'relu3_1', 'conv3_2', 'relu3_2', 'conv3_3', 'relu3_3', 'conv3_4', 'relu3_4', 'pool3', 'conv4_1', 'relu4_1', 'conv4_2', 'relu4_2', 'conv4_3', 'relu4_3', 'conv4_4', 'relu4_4', 'pool4', 'conv5_1', 'relu5_1', 'conv5_2', 'relu5_2', 'conv5_3', 'relu5_3', 'conv5_4', 'relu5_4' ) data = scipy.io.loadmat(data_path) mean = data['normalization'][0][0][0] mean_pixel = np.mean(mean, axis=(0, 1)) weights = data['layers'][0] net = {} current = input_image for i, name in enumerate(layers): kind = name[:4] if kind == 'conv': kernels, bias = weights[i][0][0][0][0] # matconvnet: weights are [width, height, in_channels, out_channels] # tensorflow: weights are [height, width, in_channels, out_channels] kernels = np.transpose(kernels, (1, 0, 2, 3)) bias = bias.reshape(-1) current = _conv_layer(current, kernels, bias) elif kind == 'relu': current = tf.nn.relu(current) elif kind == 'pool': current = _pool_layer(current) net[name] = current assert len(net) == len(layers) return net, mean_pixel def _conv_layer(input, weights, bias): conv = tf.nn.conv2d(input, tf.constant(weights), strides=(1, 1, 1, 1), padding='SAME') return tf.nn.bias_add(conv, bias) def _pool_layer(input): return tf.nn.max_pool(input, ksize=(1, 2, 2, 1), strides=(1, 2, 2, 1), padding='SAME') def preprocess(image, mean_pixel): return image - mean_pixel def unprocess(image, mean_pixel): return image + mean_pixel
總結(jié)
在本章中,我們一直在學(xué)習(xí)不同的深度神經(jīng)網(wǎng)絡(luò)架構(gòu)。
我們了解了如何構(gòu)建近年來最著名的架構(gòu)之一 VGG,以及如何使用它來生成可轉(zhuǎn)換藝術(shù)風(fēng)格的圖像。
在下一章中,我們將使用機(jī)器學(xué)習(xí)中最有用的技術(shù)之一:圖形處理單元。 我們將回顧安裝具有 GPU 支持的 TensorFlow 所需的步驟并對其進(jìn)行訓(xùn)練,并將執(zhí)行時間與唯一運(yùn)行的模型 CPU 進(jìn)行比較。
九、構(gòu)建圖像超分辨率應(yīng)用
還記得上次和親人一起旅行并拍了一些漂亮的照片作為記憶,但是當(dāng)您回到家并刷過它們時,您發(fā)現(xiàn)它們非常模糊且質(zhì)量低下嗎? 現(xiàn)在,您剩下的所有美好時光就是您自己的心理記憶和那些模糊的照片。 如果可以使您的照片清晰透明并且可以看到其中的每個細(xì)節(jié),那不是很好嗎?
超分辨率是基于像素信息的近似將低分辨率圖像轉(zhuǎn)換為高分辨率圖像的過程。 雖然今天可能還不完全是神奇的,但當(dāng)技術(shù)發(fā)展到足以成為通用 AI 應(yīng)用時,它肯定會在將來挽救生命。
在此項(xiàng)目中,我們將構(gòu)建一個應(yīng)用,該應(yīng)用使用托管在 DigitalOcean Droplet 上的深度學(xué)習(xí)模型,該模型可以同時比較低分辨率和高分辨率圖像,從而使我們更好地了解今天的技術(shù)。 我們將使用生成對抗網(wǎng)絡(luò)(GAN)生成超分辨率圖像。
在本章中,我們將介紹以下主題:
- 基本項(xiàng)目架構(gòu)
- 了解 GAN
- 了解圖像超分辨率的工作原理
- 創(chuàng)建 TensorFlow 模型以實(shí)現(xiàn)超分辨率
- 構(gòu)建應(yīng)用的 UI
- 從設(shè)備的本地存儲中獲取圖片
- 在 DigitalOcean 上托管 TensorFlow 模型
- 在 Flutter 上集成托管的自定義模型
- 創(chuàng)建材質(zhì)應(yīng)用
讓我們從了解項(xiàng)目的架構(gòu)開始。
基本項(xiàng)目架構(gòu)
讓我們從了解項(xiàng)目的架構(gòu)開始。
我們將在本章中構(gòu)建的項(xiàng)目主要分為兩個部分:
- Jupyter 筆記本,它創(chuàng)建執(zhí)行超分辨率的模型。
- 使用該模型的 Flutter 應(yīng)用,在 Jupyter 筆記本上接受訓(xùn)練后,將托管在 DigitalOcean 中的 Droplet 中。
從鳥瞰圖可以用下圖描述該項(xiàng)目:
將低分辨率圖像放入模型中,該模型是從 Firebase 上托管的 ML Kit 實(shí)例中獲取的,并放入 Flutter 應(yīng)用中。 生成輸出并將其作為高分辨率圖像顯示給用戶。 該模型緩存在設(shè)備上,并且僅在開發(fā)人員更新模型時才更新,因此可以通過減少網(wǎng)絡(luò)延遲來加快預(yù)測速度。
現(xiàn)在,讓我們嘗試更深入地了解 GAN。
了解 GAN
Ian Goodfellow,Yoshua Bengio 和其他人在 NeurIPS 2014 中引入的 GAN 席卷全球。 可以應(yīng)用于各種領(lǐng)域的 GAN 會根據(jù)模型對實(shí)際數(shù)據(jù)樣本的學(xué)習(xí)近似,生成新的內(nèi)容或序列。 GAN 已被大量用于生成音樂和藝術(shù)的新樣本,例如下圖所示的面孔,而訓(xùn)練數(shù)據(jù)集中不存在這些面孔:
經(jīng)過 60 個周期的訓(xùn)練后,GAN 生成的面孔。 該圖像取自這里。
前面面孔中呈現(xiàn)的大量真實(shí)感證明了 GAN 的力量–在為他們提供良好的訓(xùn)練樣本量之后,他們幾乎可以學(xué)習(xí)生成任何類型的模式。
GAN 的核心概念圍繞兩個玩家玩游戲的想法。 在這個游戲中,一個人說出一個隨機(jī)句子,另一個人僅僅考慮第一人稱使用的單詞就指出它是事實(shí)還是假。 第二個人唯一可以使用的知識是假句子和實(shí)句中常用的單詞(以及如何使用)。 這可以描述為由 minimax 算法玩的兩人游戲,其中每個玩家都試圖以其最大能力抵消另一位玩家所做的移動。 在 GAN 中,第一個玩家是生成器(G
),第二個玩家是判別器(D
)。 G
和D
都是常規(guī) GAN 中的神經(jīng)網(wǎng)絡(luò)。 生成器從訓(xùn)練數(shù)據(jù)集中給出的樣本中學(xué)習(xí),并基于其認(rèn)為當(dāng)觀察者查看時可以作為真實(shí)樣本傳播的樣本來生成新樣本。
判別器從訓(xùn)練樣本(正樣本)和生成器生成的樣本(負(fù)樣本)中學(xué)習(xí),并嘗試對哪些圖像存在于數(shù)據(jù)集中以及哪些圖像進(jìn)行分類。 它從G
獲取生成的圖像,并嘗試將其分類為真實(shí)圖像(存在于訓(xùn)練樣本中)或生成圖像(不存在于數(shù)據(jù)庫中)。
通過反向傳播,GAN 嘗試不斷減少判別器能夠?qū)ι善髡_生成的圖像進(jìn)行分類的次數(shù)。 一段時間后,我們希望達(dá)到識別器在識別生成的圖像時開始表現(xiàn)不佳的階段。 這是 GAN 停止學(xué)習(xí)的地方,然后可以使用生成器生成所需數(shù)量的新樣本。 因此,訓(xùn)練 GAN 意味著訓(xùn)練生成器以從隨機(jī)輸入產(chǎn)生輸出,從而使判別器無法將其識別為生成的圖像。
判別器將傳遞給它的所有圖像分為兩類:
- 真實(shí)圖像:數(shù)據(jù)集中存在的圖像或使用相機(jī)拍攝的圖像
- 偽圖像:使用某軟件生成的圖像
生成器欺騙判別器的能力越好,當(dāng)向其提供任何隨機(jī)輸入序列時,生成的輸出將越真實(shí)。
讓我們以圖表形式總結(jié)前面關(guān)于 GAN 進(jìn)行的討論:
GAN 具有許多不同的變體,所有變體都取決于它們正在執(zhí)行的任務(wù)。 其中一些如下:
- 漸進(jìn)式 GAN:在 ICLR 2018 上的一篇論文中介紹,漸進(jìn)式 GAN 的生成器和判別器均以低分辨率圖像開始,并隨著圖像層的增加而逐漸受到訓(xùn)練,從而使系統(tǒng)能夠生成高分辨率圖像。 例如,在第一次迭代中生成的圖像為
10x10
像素,在第二代中它變?yōu)?code>20x20,依此類推,直到獲得非常高分辨率的圖像為止。 生成器和判別器都在深度上一起增長。 - 條件 GAN:假設(shè)您有一個 GAN 可以生成 10 個不同類別的樣本,但是在某個時候,您希望它在給定類別或一組類別內(nèi)生成樣本。 這是有條件 GAN 起作用的時候。有條件 GAN 使我們可以生成 GAN 中經(jīng)過訓(xùn)練可以生成的所有標(biāo)簽中任何給定標(biāo)簽的樣本。 在圖像到圖像的翻譯領(lǐng)域中,已經(jīng)完成了條件 GAN 的一種非常流行的應(yīng)用,其中將一個圖像生成為相似或相同域的另一個更逼真的圖像。 您可以通過這個頁面上的演示來嘗試涂鴉一些貓,并獲得涂鴉的真實(shí)感版本。
- 棧式 GAN:棧式 GAN 的最流行的應(yīng)用是基于文本描述生成圖像。 在第一階段,GAN 生成描述項(xiàng)的概述,在第二階段,根據(jù)描述添加顏色。 然后,后續(xù)層中的 GAN 將更多細(xì)節(jié)添加到圖像中,以生成圖像的真實(shí)感版本,如描述中所述。 通過觀察堆疊 GAN 的第一次迭代中的圖像已經(jīng)處于將要生成最終輸出的尺寸,可以將棧式 GAN 與漸進(jìn)式 GAN 區(qū)別開來。但是,與漸進(jìn)式 GAN 相似,在第一次迭代中, 圖像是最小的,并且需要進(jìn)一步的層才能將其饋送到判別器。
在此項(xiàng)目中,我們將討論 GAN 的另一種形式,稱為超分辨率 GAN(SRGAN)。 我們將在下一部分中了解有關(guān)此變體的更多信息。
了解圖像超分辨率的工作原理
幾十年來,人們一直在追求并希望能夠使低分辨率圖像更加精細(xì),以及使高分辨率圖像化。 超分辨率是用于將低分辨率圖像轉(zhuǎn)換為超高分辨率圖像的技術(shù)的集合,是圖像處理工程師和研究人員最激動人心的工作領(lǐng)域之一。 已經(jīng)建立了幾種方法和方法來實(shí)現(xiàn)圖像的超分辨率,并且它們都朝著自己的目標(biāo)取得了不同程度的成功。 然而,近來,隨著 SRGAN 的發(fā)展,關(guān)于使用任何低分辨率圖像可以實(shí)現(xiàn)的超分辨率的量有了顯著的改進(jìn)。
但是在討論 SRGAN 之前,讓我們了解一些與圖像超分辨率有關(guān)的概念。
了解圖像分辨率
用質(zhì)量術(shù)語來說,圖像的分辨率取決于其清晰度。 分辨率可以歸類為以下之一:
- 像素分辨率
- 空間分辨率
- 時間分辨率
- 光譜分辨率
- 輻射分辨率
讓我們來看看每個。
像素分辨率
指定分辨率的最流行格式之一,像素分辨率最通常是指形成圖像時涉及的像素數(shù)量。 單個像素是可以在任何給定查看設(shè)備上顯示的最小單個單元。 可以將幾個像素組合在一起以形成圖像。 在本書的前面,我們討論了圖像處理,并將像素稱為存儲在矩陣中的顏色信息的單個單元,它代表圖像。 像素分辨率定義了形成數(shù)字圖像所需的像素元素總數(shù),該總數(shù)可能與圖像上可見的有效像素數(shù)不同。
標(biāo)記圖像像素分辨率的一種非常常見的表示法是以百萬像素表示。 給定NxM
像素分辨率的圖像,其分辨率可以寫為(NxM / 1000000
)百萬像素。 因此,尺寸為2,000x3,000
的圖像將具有 6,000,000 像素,其分辨率可以表示為 6 兆像素。
空間分辨率
這是觀察圖像的人可以分辨圖像中緊密排列的線條的程度的度量。 在這里,嚴(yán)格說來,圖像的像素越多,清晰度越好。 這是由于具有較高像素數(shù)量的圖像的空間分辨率較低。 因此,需要良好的空間分辨率以及具有良好的像素分辨率以使圖像以良好的質(zhì)量呈現(xiàn)。
它也可以定義為像素一側(cè)所代表的距離量。
時間分辨率
分辨率也可能取決于時間。 例如,衛(wèi)星或使用無人飛行器(UAV)無人機(jī)拍攝的同一區(qū)域的圖像可能會隨時間變化。 重新捕獲相同區(qū)域的圖像所需的時間稱為時間分辨率。
時間分辨率主要取決于捕獲圖像的設(shè)備。 如在圖像捕捉的情況下,這可以是變型,例如當(dāng)在路邊的速度陷阱照相機(jī)中觸發(fā)特定傳感器時執(zhí)行圖像捕捉。 它也可以是常數(shù)。 例如,在配置為每x
間隔拍照的相機(jī)中。
光譜分辨率
光譜分辨率是指圖像捕獲設(shè)備可以記錄的波段數(shù)。 也可以將其定義為波段的寬度或每個波段的波長范圍。 在數(shù)字成像方面,光譜分辨率類似于圖像中的通道數(shù)。 理解光譜分辨率的另一種方法是在任何給定圖像或頻帶記錄中可區(qū)分的頻帶數(shù)。
黑白圖像中的波段數(shù)為 1,而彩色(RGB)圖像中的波段數(shù)為 3??梢圆东@數(shù)百個波段的圖像,其中其他波段可提供有關(guān)圖像的不同種類的信息。 圖片。
輻射分辨率
輻射分辨率是捕獲設(shè)備表示在任何頻帶/通道上接收到的強(qiáng)度的能力。 輻射分辨率越高,設(shè)備可以更準(zhǔn)確地捕獲其通道上的強(qiáng)度,并且圖像越真實(shí)。
輻射分辨率類似于圖像每個像素的位數(shù)。 雖然 8 位圖像像素可以表示 256 個不同的強(qiáng)度,但是 256 位圖像像素可以表示2 ^ 256
個不同的強(qiáng)度。 黑白圖像的輻射分辨率為 1 位,這意味著每個像素只能有兩個不同的值,即 0 和 1。
現(xiàn)在,讓我們嘗試了解 SRGAN。
了解 SRGAN
SRGAN 是一類 GAN,主要致力于從低分辨率圖像創(chuàng)建超分辨率圖像。
SRGAN 算法的功能描述如下:該算法從數(shù)據(jù)集中選取高分辨率圖像,然后將其采樣為低分辨率圖像。 然后,生成器神經(jīng)網(wǎng)絡(luò)嘗試從低分辨率圖像生成高分辨率圖像。 從現(xiàn)在開始,我們將其稱為超分辨率圖像。 將超分辨率圖像發(fā)送到鑒別神經(jīng)網(wǎng)絡(luò),該神經(jīng)網(wǎng)絡(luò)已經(jīng)在高分辨率圖像和一些基本的超分辨率圖像的樣本上進(jìn)行了訓(xùn)練,以便可以對它們進(jìn)行分類。
判別器將由生成器發(fā)送給它的超分辨率圖像分類為有效的高分辨率圖像,偽高分辨率圖像或超分辨率圖像。 如果將圖像分類為超分辨率圖像,則 GAN 損失會通過生成器網(wǎng)絡(luò)反向傳播,以便下次產(chǎn)生更好的偽造圖像。 隨著時間的流逝,生成器將學(xué)習(xí)如何創(chuàng)建更好的偽造品,并且判別器開始無法正確識別超分辨率圖像。 GAN 在這里停止學(xué)習(xí),被列為受過訓(xùn)練的人。
可以用下圖來總結(jié):
現(xiàn)在,讓我們開始創(chuàng)建用于超分辨率的 SRGAN 模型。
創(chuàng)建 TensorFlow 模型來實(shí)現(xiàn)超分辨率
現(xiàn)在,我們將開始構(gòu)建在圖像上執(zhí)行超分辨率的 GAN 模型。 在深入研究代碼之前,我們需要了解如何組織項(xiàng)目目錄。
項(xiàng)目目錄結(jié)構(gòu)
本章中包含以下文件和文件夾:
api/
:model /
:__init __.py
:此文件指示此文件的父文件夾可以像模塊一樣導(dǎo)入。common.py
:包含任何 GAN 模型所需的常用函數(shù)。srgan.py
:其中包含開發(fā) SRGAN 模型所需的函數(shù)。weights/
:gan_generator.h5
:模型的預(yù)訓(xùn)練權(quán)重文件。 隨意使用它來快速運(yùn)行并查看項(xiàng)目的工作方式。data.py
:用于在 DIV2K 數(shù)據(jù)集中下載,提取和加載圖像的工具函數(shù)。flask_app.py
:我們將使用此文件來創(chuàng)建將在 DigitalOcean 上部署的服務(wù)器。train.py
:模型訓(xùn)練文件。 我們將在本節(jié)中更深入地討論該文件。
您可以在這個頁面中找到項(xiàng)目此部分的源代碼。
多樣 2K(DIV2K)數(shù)據(jù)集由圖像恢復(fù)和增強(qiáng)的新趨勢(NTIRE)2017 單張圖像超分辨率挑戰(zhàn)賽引入,也用于挑戰(zhàn)賽的 2018 版本中。
在下一節(jié)中,我們將構(gòu)建 SRGAN 模型腳本。
創(chuàng)建用于超分辨率的 SRGAN 模型
首先,我們將從處理train.py
文件開始:
- 讓我們從將必要的模塊導(dǎo)入項(xiàng)目開始:
import osfrom data import DIV2K
from model.srgan import generator, discriminator
from train import SrganTrainer, SrganGeneratorTrainer
前面的導(dǎo)入引入了一些現(xiàn)成的類,例如SrganTrainer
,SrganGeneratorTrainer
等。 在完成此文件的工作后,我們將詳細(xì)討論它們。
- 現(xiàn)在,讓我們?yōu)闄?quán)重創(chuàng)建一個目錄。 我們還將使用此目錄來存儲中間模型:
weights_dir = 'weights'
weights_file = lambda filename: os.path.join(weights_dir, filename)os.makedirs(weights_dir, exist_ok=True)
- 接下來,我們將從 DIV2K 數(shù)據(jù)集中下載并加載圖像。 我們將分別下載訓(xùn)練和驗(yàn)證圖像。 對于這兩組圖像,可以分為兩對:高分辨率和低分辨率。 但是,這些是單獨(dú)下載的:
div2k_train = DIV2K(scale=4, subset='train', downgrade='bicubic')
div2k_valid = DIV2K(scale=4, subset='valid', downgrade='bicubic')
- 將數(shù)據(jù)集下載并加載到變量后,我們需要將訓(xùn)練圖像和驗(yàn)證圖像都轉(zhuǎn)換為 TensorFlow 數(shù)據(jù)集對象。 此步驟還將兩個數(shù)據(jù)集中的高分辨率和低分辨率圖像結(jié)合在一起:
train_ds = div2k_train.dataset(batch_size=16, random_transform=True)
valid_ds = div2k_valid.dataset(batch_size=16, random_transform=True, repeat_count=1)
- 現(xiàn)在,回想一下我們在“了解 GAN”部分中提供的 GAN 的定義。 為了使生成器開始產(chǎn)生判別器可以評估的偽造品,它需要學(xué)習(xí)創(chuàng)建基本的偽造品。 為此,我們將快速訓(xùn)練神經(jīng)網(wǎng)絡(luò),以便它可以生成基本的超分辨率圖像。 我們將其命名為預(yù)訓(xùn)練器。 然后,我們將預(yù)訓(xùn)練器的權(quán)重遷移到實(shí)際的 SRGAN,以便它可以通過使用判別器來學(xué)習(xí)更多。 讓我們構(gòu)建并運(yùn)行預(yù)訓(xùn)練器:
pre_trainer = SrganGeneratorTrainer(model=generator(), checkpoint_dir=f'.ckpt/pre_generator')
pre_trainer.train(train_ds,valid_ds.take(10),steps=1000000, evaluate_every=1000, save_best_only=False)pre_trainer.model.save_weights(weights_file('pre_generator.h5'))
現(xiàn)在,我們已經(jīng)訓(xùn)練了一個基本模型并保存了權(quán)重。 我們可以隨時更改 SRGAN 并通過加載其權(quán)重從基礎(chǔ)訓(xùn)練中重新開始。
- 現(xiàn)在,讓我們將預(yù)訓(xùn)練器權(quán)重加載到 SRGAN 對象中,并執(zhí)行訓(xùn)練迭代:
gan_generator = generator()
gan_generator.load_weights(weights_file('pre_generator.h5'))gan_trainer = SrganTrainer(generator=gan_generator, discriminator=discriminator())
gan_trainer.train(train_ds, steps=200000)
請注意,在具有 8 GB RAM 和 Intel i7 處理器的普通計(jì)算機(jī)上,上述代碼中的訓(xùn)練操作可能會花費(fèi)大量時間。 建議在具有圖形處理器(GPU)的基于云的虛擬機(jī)中執(zhí)行此訓(xùn)練。
- 現(xiàn)在,讓我們保存 GAN 生成器和判別器的權(quán)重:
gan_trainer.generator.save_weights(weights_file('gan_generator.h5'))
gan_trainer.discriminator.save_weights(weights_file('gan_discriminator.h5'))
現(xiàn)在,我們準(zhǔn)備繼續(xù)進(jìn)行下一部分,在該部分中將構(gòu)建將使用此模型的 Flutter 應(yīng)用的 UI。
構(gòu)建應(yīng)用的 UI
現(xiàn)在,我們了解了圖像超分辨率模型的基本功能并為其創(chuàng)建了一個模型,讓我們深入研究構(gòu)建 Flutter 應(yīng)用。 在本節(jié)中,我們將構(gòu)建應(yīng)用的 UI。
該應(yīng)用的用戶界面非常簡單:它將包含兩個圖像小部件和按鈕小部件。 當(dāng)用戶單擊按鈕小部件時,他們將能夠從設(shè)備的庫中選擇圖像。 相同的圖像將作為輸入發(fā)送到托管模型的服務(wù)器。 服務(wù)器將返回增強(qiáng)的圖像。 屏幕上將放置的兩個圖像小部件將用于顯示服務(wù)器的輸入和服務(wù)器的輸出。
下圖說明了應(yīng)用的基本結(jié)構(gòu)和最終流程:
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-mNqyudKm-1681785128426)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mobi-dl-tflite/img/86a43bbe-4673-4dcb-8a8c-591d7c952df0.png)]
該應(yīng)用的三個主要小部件可以簡單地排列在一列中。 該應(yīng)用的小部件樹如下所示:
現(xiàn)在,讓我們編寫代碼以構(gòu)建主屏幕。 以下步驟討論了該應(yīng)用小部件的創(chuàng)建和放置:
- 首先,我們創(chuàng)建一個名為
image_super_resolution.dart
的新文件。 這將包含一個名為ImageSuperResolution
的無狀態(tài)窗口小部件。 該小部件將包含應(yīng)用主屏幕的代碼。 - 接下來,我們將定義一個名為
buildImageInput()
的函數(shù),該函數(shù)返回一個小部件,該小部件負(fù)責(zé)顯示用戶選擇的圖像:
Widget buildImage1() {return Expanded(child: Container(width: 200, height: 200, child: img1));}
此函數(shù)返回帶有Container
作為其child.
的Expanded
小部件。Container
的width
和height
為200
。 Container
的子元素最初是存儲在資產(chǎn)文件夾中的占位符圖像,可以通過img1
變量進(jìn)行訪問,如下所示:
var img1 = Image.asset('assets/place_holder_image.png');
我們還將在pubspec.yaml
文件中添加圖像的路徑,如下所示:
flutter:assets:- assets/place_holder_image.png
- 現(xiàn)在,我們將創(chuàng)建另一個函數(shù)
buildImageOutput()
,該函數(shù)返回一個小部件,該小部件負(fù)責(zé)顯示模型返回的增強(qiáng)圖像:
Widget buildImageOutput() {return Expanded(child: Container(width: 200, height: 200, child: imageOutput));}
此函數(shù)返回一個以其Container
作為其子元素的Expanded
小部件。 Container
的寬度和高度設(shè)置為200
。 Container
的子級是名為imageOutput
的小部件。 最初,imageOutput
還將包含一個占位符圖像,如下所示:
Widget imageOutput = Image.asset('assets/place_holder_image.png');
將模型集成到應(yīng)用中后,我們將更新imageOutput
。
- 現(xiàn)在,我們將定義第三個函數(shù)
buildPickImageButton()
,該函數(shù)返回一個Widget
,我們可以使用它從設(shè)備的圖庫中選擇圖像:
Widget buildPickImageButton() {return Container(margin: EdgeInsets.all(8),child: FloatingActionButton(elevation: 8,child: Icon(Icons.camera_alt),onPressed: () => {},));}
此函數(shù)返回以FloatingActionButton
作為其子元素的Container
。 按鈕的elevation
屬性控制其下方陰影的大小,并設(shè)置為8
。 為了反映該按鈕用于選擇圖像,通過Icon
類為它提供了攝像機(jī)的圖標(biāo)。 當(dāng)前,我們已經(jīng)將按鈕的onPressed
屬性設(shè)置為空白。 我們將在下一部分中定義一個函數(shù),使用戶可以在按下按鈕時從設(shè)備的圖庫中選擇圖像。
- 最后,我們將覆蓋
build
方法以返回應(yīng)用的Scaffold
:
@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text('Image Super Resolution')),body: Container(child: Column(crossAxisAlignment: CrossAxisAlignment.center,children: <Widget>[buildImageInput(),buildImageOutput(),buildPickImageButton()])));}
Scaffold
包含一個appBar
,其標(biāo)題設(shè)置為“圖像超分辨率”。 Scaffold
的主體為Container
,其子代為Column
。 該列的子級是我們在先前步驟中構(gòu)建的三個小部件。 另外,我們將Column
的crossAxisAlignment
屬性設(shè)置為CrossAxisAlignment.center
,以確保該列位于屏幕的中央。
至此,我們已經(jīng)成功構(gòu)建了應(yīng)用的初始狀態(tài)。 以下屏幕截圖顯示了該應(yīng)用現(xiàn)在的外觀:
盡管屏幕看起來很完美,但目前無法正常工作。 接下來,我們將向應(yīng)用添加功能。 我們將添加讓用戶從圖庫中選擇圖像的功能。
從設(shè)備的本地存儲中獲取圖片
在本節(jié)中,我們將添加FloatingActionButton
的功能,以使用戶可以從設(shè)備的圖庫中選擇圖像。 這最終將被發(fā)送到服務(wù)器,以便我們能夠收到響應(yīng)。
以下步驟描述了如何啟動圖庫并讓用戶選擇圖像:
- 為了允許用戶從設(shè)備的圖庫中選擇圖像,我們將使用
image_picker
庫。 這將啟動圖庫并存儲用戶選擇的圖像文件。 我們將從在pubspec.yaml
文件中添加依賴項(xiàng)開始:
image_picker: 0.4.12+1
另外,我們通過在終端上運(yùn)行flutter pub get
來獲取庫。
- 接下來,我們將庫導(dǎo)入
image_super_resolution.dart
文件中:
import 'package:image_picker/image_picker.dart';
- 現(xiàn)在,讓我們定義
pickImage()
函數(shù),該函數(shù)使用戶可以從圖庫中選擇圖像:
void pickImage() async {File pickedImg = await ImagePicker.pickImage(source: ImageSource.gallery);}
- 從函數(shù)內(nèi)部,我們只需調(diào)用
ImagePicker.pickImage()
并將source
指定為ImageSource.gallery
即可。 該庫本身處理啟動設(shè)備圖庫的復(fù)雜性。 用戶選擇的圖像文件最終由該函數(shù)返回。 我們將函數(shù)返回的文件存儲在File
類型的pickedImg
變量中。 - 接下來,我們定義
loadImage()
函數(shù),以便在屏幕上顯示用戶選擇的圖像:
void loadImage(File file) {setState(() {img1 = Image.file(file);});}
此函數(shù)將用戶選擇的圖像文件作為輸入。 在函數(shù)內(nèi)部,我們將先前聲明的img1
變量的值設(shè)置為Image.file(file)
,這將返回從'file'
構(gòu)建的Image
小部件。 回想一下,最初,img1
被設(shè)置為占位符圖像。 為了重新渲染屏幕并顯示用戶選擇的圖像,我們將img1
的新分配放在setState()
中。
- 現(xiàn)在,將
pickImage()
添加到builtPickImageButton()
內(nèi)的FloatingActionButton
的onPressed
屬性中:
Widget buildPickImageButton() {return Container(....child: FloatingActionButton(....onPressed: () => pickImage(),));}
前面的補(bǔ)充內(nèi)容確保單擊按鈕時,會啟動圖庫,以便可以選擇圖像。
- 最后,我們將從
pickImage()
向loadImage()
添加一個調(diào)用:
void pickImage() async {....loadImage(pickedImg);}
在loadImage()
內(nèi)部,我們傳入用戶選擇的圖像,該圖像存儲在pickedImage
變量中,以便可以在應(yīng)用的屏幕上查看該圖像。
完成上述所有步驟后,該應(yīng)用將如下所示:
至此,我們已經(jīng)構(gòu)建了應(yīng)用的用戶界面。 我們還添加了一些功能,使用戶可以從設(shè)備的圖庫中選擇圖像并將其顯示在屏幕上。
在下一部分中,我們將學(xué)習(xí)如何托管在“為超分辨率創(chuàng)建 TensorFlow 模型”中創(chuàng)建的模型作為 API,以便我們可以使用它執(zhí)行超分辨率。
在 DigitalOcean 上托管 TensorFlow 模型
DigitalOcean 是一個了不起的低成本云解決方案平臺,非常易于上手,并提供了應(yīng)用開發(fā)人員為立即可用的應(yīng)用后端提供動力所需的幾乎所有功能。 該界面非常易于使用,并且 DigitalOcean 擁有一些最廣泛的文檔,這些文檔圍繞著如何在云上設(shè)置不同類型的應(yīng)用服務(wù)器提供入門。
在這個項(xiàng)目中,我們將使用 DigitalOcean 的 Droplet 部署我們的超分辨率 API。 DigitalOcean 中的 Droplet 只是通常在共享硬件空間上運(yùn)行的虛擬機(jī)。
首先,我們將在項(xiàng)目目錄中創(chuàng)建flask_app.py
文件,并添加服務(wù)器工作所需的代碼。
創(chuàng)建一個 Flask 服務(wù)器腳本
在本節(jié)中,我們將處理flask_app.py
文件,該文件將作為服務(wù)器在云虛擬機(jī)上運(yùn)行。 讓我們開始吧:
- 首先,我們將對文件進(jìn)行必要的導(dǎo)入:
from flask import Flask, request, jsonify, send_file
import os
import timefrom matplotlib.image import imsavefrom model.srgan import generatorfrom model import resolve_single
- 現(xiàn)在,我們將定義
weights
目錄并將生成器權(quán)重加載到文件中:
weights_dir = 'weights'
weights_file = lambda filename: os.path.join(weights_dir, filename)gan_generator = generator()
gan_generator.load_weights(weights_file('gan_generator.h5'))
- 接下來,我們將使用以下代碼行實(shí)例化
Flask
應(yīng)用:
app = Flask(__name__)
- 現(xiàn)在,我們準(zhǔn)備構(gòu)建服務(wù)器將監(jiān)聽的路由。 首先,我們將創(chuàng)建
/generate
路由,該路由將圖像作為輸入,生成其超分辨率版本,并將所生成的高分辨率圖像的文件名返回給用戶:
@app.route('/generate', methods=["GET", "POST"])
def generate():global gan_generatorimgData = request.get_data()with open("input.png", 'wb') as output:output.write(imgData)lr = load_image("input.png")gan_sr = resolve_single(gan_generator, lr)epoch_time = int(time.time())outputfile = 'output_%s.png' % (epoch_time)imsave(outputfile, gan_sr.numpy())response = {'result': outputfile}return jsonify(response)
讓我們嘗試了解前面的代碼塊中發(fā)生的情況。 /generate
路由已設(shè)置為僅監(jiān)聽 HTTP 請求的 GET 和 POST 方法。 首先,該方法獲取 API 請求中提供給它的圖像,將其轉(zhuǎn)換為 NumPy 數(shù)組,然后將其提供給 SRGAN 模型。 SRGAN 模型返回超分辨率圖像,然后為其分配一個唯一的名稱并存儲在服務(wù)器上。 用戶顯示文件名,他們可以使用該文件名調(diào)用另一個端點(diǎn)來下載文件。 讓我們現(xiàn)在構(gòu)建此端點(diǎn)。
- 為了創(chuàng)建端點(diǎn)以便下載生成的文件,我們可以使用以下代碼:
@app.route('/download/<fname>', methods=['GET'])
def download(fname):return send_file(fname)
在這里,我們創(chuàng)建了一個名為/download
的端點(diǎn),該端點(diǎn)附加了文件名后,將其提取并發(fā)送回給用戶。
- 最后,我們可以編寫執(zhí)行該腳本并設(shè)置服務(wù)器的代碼:
app.run(host="0.0.0.0", port="8080")
保存此文件。 確保此時將您的存儲庫推送到 GitHub/GitLab 存儲庫。
現(xiàn)在,我們準(zhǔn)備將該腳本部署到DigitalOcean
Droplet。
將 Flask 腳本部署到 DigitalOcean Droplet
要將 Flask 腳本部署到 DigitalOcean Droplet,您必須創(chuàng)建一個 DigitalOcean 帳戶并創(chuàng)建一個 Droplet。 請按照以下步驟操作:
- 在您喜歡的 Web 瀏覽器中轉(zhuǎn)到 digitalocean.com 。
如果您希望在添加帳單詳細(xì)信息時獲得 100 美元的贈金,也可以轉(zhuǎn)到這里。 我們稍后再做。
-
在 DigitalOcean 的注冊表格中填寫您的詳細(xì)信息,然后提交表格繼續(xù)進(jìn)行下一步。
-
系統(tǒng)將要求您驗(yàn)證電子郵件并為 DigitalOcean 帳戶添加結(jié)算方式。
-
在下一步中,系統(tǒng)將提示您創(chuàng)建第一個項(xiàng)目。 輸入所需的詳細(xì)信息并提交表單以創(chuàng)建您的項(xiàng)目:
- 創(chuàng)建項(xiàng)目后,您將被帶到 DigitalOcean 儀表板。 您將能夠看到創(chuàng)建 Droplet 的提示,如以下屏幕截圖所示:
-
單擊“提示”以彈出 Droplet 創(chuàng)建表單。 選擇下表中描述的選項(xiàng):
字段 說明 要使用的值 選擇一張圖片 Droplet 將在其上運(yùn)行的操作系統(tǒng)。 Ubuntu 18.04(或最新可用版本) 選擇一個計(jì)劃 選擇 Droplet 的配置。 4 GB RAM 或更高 添加塊存儲 Droplet 的其他持久性,可拆卸存儲容量。 保留默認(rèn)值 選擇數(shù)據(jù)中心區(qū)域 投放 Droplet 的區(qū)域。 根據(jù)您的喜好選擇任何一個 選擇其他選項(xiàng) 選擇將與您的 Droplet 一起使用的所有其他功能。 保留默認(rèn)值 認(rèn)證方式 選擇虛擬機(jī)的認(rèn)證方法。 一次性密碼 完成并創(chuàng)建 Droplet 的一些其他設(shè)置和選項(xiàng)。 保留默認(rèn)值 -
單擊“創(chuàng)建 Droplet”,然后等待 DigitalOcean 設(shè)置您的 Droplet。
-
創(chuàng)建 Droplet 后,單擊其名稱以打開 Droplet 管理控制臺,該控制臺應(yīng)如下所示:
-
現(xiàn)在,我們可以使用上一幅截圖所示的 Droplet 控制臺左側(cè)導(dǎo)航菜單上的 Access 選項(xiàng)卡登錄到 Droplet。 單擊“訪問”,然后啟動控制臺。
-
將打開一個新的瀏覽器窗口,顯示您的 Droplet 的 VNC 視圖。 系統(tǒng)將要求您輸入 Droplet 的用戶名和密碼。 您必須在此處使用的用戶名是
root
。 可以在您已注冊的電子郵件收件箱中找到該密碼。 -
首次登錄時,系統(tǒng)會要求您更改 Droplet 密碼。 確保您選擇一個強(qiáng)密碼。
-
登錄 Droplet 后,將在 VNC 終端上看到一些 Ubuntu 歡迎文本,如以下屏幕截圖所示:
- 現(xiàn)在,按照本書的“附錄”中的說明,執(zhí)行在云 VM 上設(shè)置深度學(xué)習(xí)環(huán)境的步驟。
- 接下來,將項(xiàng)目存儲庫克隆到您的 Droplet,并使用以下命令將工作目錄更改為存儲庫的
api
文件夾:
git clone https://github.com/yourusername/yourrepo.git
cd yourrepo/api
- 使用以下命令運(yùn)行服務(wù)器:
python3 flask_app.py
除了來自 TensorFlow 的一些警告消息之外,在終端輸出的末尾,您還應(yīng)該看到以下幾行指示服務(wù)器已成功啟動:
現(xiàn)在,如 Droplet 控制臺所示,您的服務(wù)器已啟動并在 Droplet 的 IP 上運(yùn)行。
在下一部分中,我們將學(xué)習(xí)如何使用 Flutter 應(yīng)用向服務(wù)器發(fā)出 POST 請求,并在屏幕上顯示服務(wù)器的響應(yīng)。
在 Flutter 上集成托管的自定義模型
在本節(jié)中,我們將向托管模型發(fā)出 POST 請求,并將其傳遞給用戶選擇的圖像。 服務(wù)器將以 PNG 格式響應(yīng)NetworkImage
。 然后,我們將更新之前添加的圖像小部件,以顯示模型返回的增強(qiáng)圖像。
讓我們開始將托管模型集成到應(yīng)用中:
- 首先,我們將需要兩個以上的外部庫來發(fā)出成功的 POST 請求。 因此,我們將以下庫作為依賴項(xiàng)添加到
pubspec.yaml
文件:
dependencies:flutter:http: 0.12.0+4mime: 0.9.6+3
http
依賴項(xiàng)包含一組類和函數(shù),這些類和函數(shù)使使用 HTTP 資源非常方便。 mime
依賴性用于處理 MIME 多部分媒體類型的流。
現(xiàn)在,我們需要運(yùn)行flutter pub get
以確保所有依賴項(xiàng)均已正確安裝到我們的項(xiàng)目中。
- 接下來,我們將所有新添加的依賴項(xiàng)導(dǎo)入
image_super_resolution.dart
文件:
import 'package:http/http.dart' as http;
import 'package:mime/mime.dart';
- 現(xiàn)在,我們需要定義
fetchResponse()
,它接受所選的圖像文件并向服務(wù)器創(chuàng)建 POST 請求:
void fetchResponse(File image) async {final mimeTypeData =lookupMimeType(image.path, headerBytes: [0xFF, 0xD8]).split('/');final imageUploadRequest = http.MultipartRequest('POST', Uri.parse("http://x.x.x.x:8080/generate"));final file = await http.MultipartFile.fromPath('image', image.path,contentType: MediaType(mimeTypeData[0], mimeTypeData[1]));imageUploadRequest.fields['ext'] = mimeTypeData[1];imageUploadRequest.files.add(file);try {final streamedResponse = await imageUploadRequest.send();final response = await http.Response.fromStream(streamedResponse);final Map<String, dynamic> responseData = json.decode(response.body); String outputFile = responseData['result'];} catch (e) {print(e);return null;}}
在前面的方法中,我們通過使用lookupMimeType
函數(shù)并使用文件的路徑及其頭來查找所選文件的 MIME 類型。 然后,按照托管模型的服務(wù)器的預(yù)期,初始化一個多部分請求。 我們使用 HTTP 執(zhí)行此操作。 我們使用MultipartFile.fromPath
并將image
的值設(shè)置為作為POST
參數(shù)附加的路徑。 由于image_picker
存在一些錯誤,因此我們將圖片的擴(kuò)展名明確傳遞給請求主體。 因此,它將圖像擴(kuò)展名與文件名(例如filenamejpeg
)混合在一起,這在管理或驗(yàn)證文件擴(kuò)展名時在服務(wù)器端造成了問題。 然后,來自服務(wù)器的響應(yīng)將存儲在response
變量中。 響應(yīng)為 JSON 格式,因此我們需要使用json.decode()
對其進(jìn)行解碼。 該函數(shù)接收響應(yīng)的主體,可以使用response.body
進(jìn)行訪問。 我們將解碼后的 JSON 存儲在responseData
變量中。 最后,使用responseDate['result']
訪問服務(wù)器的輸出并將其存儲在outputFile
變量中。
- 接下來,我們定義
displayResponseImage()
函數(shù),該函數(shù)接受服務(wù)器在outputFile
參數(shù)內(nèi)返回的 PNG 文件的名稱:
void displayResponseImage(String outputFile) {print("Updating Image");outputFile = 'http://x.x.x.x:8080/download/' + outputFile;setState(() { imageOutput = Image(image: NetworkImage(outputFile));});}
根據(jù)服務(wù)器的自定義,我們需要在文件名之前附加一個字符串以將其顯示在屏幕上。 該字符串應(yīng)包含服務(wù)器正在運(yùn)行的端口地址,后跟'/download/<outputFile>'
。 然后,我們將outputFile
的最終值用作url
值,將imageOutput
小部件的值設(shè)置為NetworkImage
。 另外,我們將其封裝在[H??TG5]中,以便在正確獲取響應(yīng)后可以刷新屏幕。
- 接下來,我們在
fetchResponse()
的最后調(diào)用displayResponseImage()
,并傳入從托管模型收到的outputFile
:
void fetchResponse(File image) async {.... displayResponseImage(outputFile);
}
- 最后,通過傳入用戶最初選擇的圖像,將調(diào)用從
pickImage()
添加到fetchResponse()
:
void pickImage() async {....fetchResponse(pickedImg);}
在前面的步驟中,我們首先向托管模型的服務(wù)器發(fā)出 POST 請求。 然后,我們解碼響應(yīng)并添加代碼以在屏幕上顯示它。 在pickImage()
末尾添加fetchResponse()
可確保僅在用戶選擇圖像后才發(fā)出 POST 請求。 另外,為了確保在成功解碼來自服務(wù)器的輸出之后已經(jīng)嘗試顯示響應(yīng)圖像,在fetchResponse()
的末尾調(diào)用displayImageResponse()
。 以下屏幕快照顯示了屏幕的最終預(yù)期狀態(tài):
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-YGKKzKRo-1681785128428)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mobi-dl-tflite/img/14248601-46e6-421b-aa88-3353ff56bd4d.png)]
因此,我們已經(jīng)完成了應(yīng)用的構(gòu)建,以便可以顯示模型的輸出。 我們將兩個圖像保存在屏幕上,以便我們可以看到它們之間的差異。
可以在這個頁面上訪問image_super_resolution.dart
文件的代碼。
創(chuàng)建材質(zhì)應(yīng)用
現(xiàn)在,我們將添加main.dart
以創(chuàng)建最終的 Material 應(yīng)用。 我們將創(chuàng)建一個名為MyApp
的無狀態(tài)小部件,并覆蓋build()
方法:
class MyApp extends StatelessWidget {@overrideWidget build(BuildContext context) {return MaterialApp(title: 'Flutter Demo',theme: ThemeData(primarySwatch: Colors.blue,),home: ImageSuperResolution(),);}
}
最后,我們執(zhí)行代碼,如下所示:
void main() => runApp(MyApp());
至此,我們完成了一個應(yīng)用的創(chuàng)建,該應(yīng)用允許用戶選擇圖像并修改其分辨率。
總結(jié)
在本章中,我們研究了超分辨率圖像以及如何使用 SRGAN 應(yīng)用它們。 我們還研究了其他類型的 GAN 以及 GAN 的總體工作方式。 然后,我們討論了如何創(chuàng)建一個 Flutter 應(yīng)用,該應(yīng)用可以與 DigitalOcean Droplet 上托管的 API 集成在一起,以便當(dāng)從圖庫中拾取圖像時可以執(zhí)行圖像超分辨率。 接下來,我們介紹了如何使用 DigitalOcean Droplet,以及由于其低成本和易于使用的界面而成為托管應(yīng)用后端的理想選擇。
在下一章中,我們將討論一些流行的應(yīng)用,這些應(yīng)用通過將深度學(xué)習(xí)集成到其功能中而獲得了很大的改進(jìn)。 我們還將探索手機(jī)深度學(xué)習(xí)中的一些熱門研究領(lǐng)域,并簡要討論已在其上進(jìn)行的最新工作。
十、前方的路
旅程中最重要的部分是知道結(jié)束后要去哪里。 到目前為止,在本系列項(xiàng)目中,我們已經(jīng)介紹了一些與 Flutter 應(yīng)用相關(guān)的獨(dú)特且功能強(qiáng)大的深度學(xué)習(xí)(DL)應(yīng)用,但重要的是,您必須知道在哪里可以找到更多這樣的項(xiàng)目,靈感和知識來構(gòu)建自己的出色項(xiàng)目。 在本章中,我們將簡要介紹當(dāng)今在移動應(yīng)用上使用 DL 的最流行的應(yīng)用,當(dāng)前趨勢以及將來在該領(lǐng)域中將會出現(xiàn)的情況。
在本章中,我們將介紹以下主題:
- 了解移動應(yīng)用中 DL 的最新趨勢
- 探索移動設(shè)備上 DL 的最新發(fā)展
- 探索移動應(yīng)用中 DL 的當(dāng)前研究領(lǐng)域
讓我們開始研究 DL 移動應(yīng)用世界中的一些趨勢。
了解移動應(yīng)用中 DL 的最新趨勢
特別是 DL,隨著最新技術(shù)和硬件的發(fā)展,人工智能(AI)變得越來越移動。 組織一直在使用智能算法來提供個性化的用戶體驗(yàn)并提高應(yīng)用參與度。 借助人臉檢測,圖像處理,文本識別,對象識別和語言翻譯等技術(shù),移動應(yīng)用已不僅僅是提供靜態(tài)信息的媒介。 它們能夠適應(yīng)用戶的個人偏好和選擇以及當(dāng)前和過去的環(huán)境狀況,以提供無縫的用戶體驗(yàn)。
讓我們看一下一些流行的應(yīng)用及其部署的方法,以提供良好的用戶體驗(yàn),同時增加應(yīng)用的參與度。
數(shù)學(xué)求解器
數(shù)學(xué)求解器應(yīng)用由微軟于 2020 年 1 月 16 日啟動,可通過簡單地單擊智能手機(jī)上有問題的圖片來幫助學(xué)生完成數(shù)學(xué)作業(yè)。 該應(yīng)用為基本和高級數(shù)學(xué)問題提供支持,涵蓋了廣泛的主題,包括基本算術(shù),二次方程,微積分和統(tǒng)計(jì)。 以下屏幕截圖顯示了該應(yīng)用的工作方式:
用戶可以在其智能手機(jī)上單擊手寫或打印問題的圖片,或直接在設(shè)備上涂鴉或鍵入圖片。 該應(yīng)用利用 AI 來識別問題并準(zhǔn)確解決。 此外,它還可以提供分步說明,并提供其他學(xué)習(xí)資料,例如與問題有關(guān)的工作表和視頻教程。
Netflix
Netflix 的推薦系統(tǒng)是在移動應(yīng)用上使用 DL 的最大成功案例之一。 Netflix 利用多種算法來了解用戶的偏好,并提供了他們可能感興趣的推薦列表。所有內(nèi)容都標(biāo)記有標(biāo)簽,這些標(biāo)簽提供了可以從中學(xué)習(xí)算法的初始數(shù)據(jù)集。 此外,該系統(tǒng)監(jiān)視著超過 1 億個用戶個人資料,以分析人們觀看的內(nèi)容,以后可能觀看的內(nèi)容,以前觀看的內(nèi)容,一年前觀看的內(nèi)容,等等。 將收集的所有數(shù)據(jù)匯總在一起,以了解用戶可能感興趣的內(nèi)容類型。
然后,將使用標(biāo)簽和用戶行為收集的數(shù)據(jù)匯總在一起,并輸入到復(fù)雜的 ML 算法中。 這些數(shù)據(jù)有助于解釋可能最重要的因素-例如,如果用戶一年前觀看的電影與上周觀看的系列相比應(yīng)被計(jì)數(shù)兩次。 該算法還可以從用戶行為中學(xué)習(xí),例如用戶喜歡還是不喜歡特定的內(nèi)容,或者用戶在 2 個晚上觀看和觀看的節(jié)目。 將所有因素匯總在一起并進(jìn)行仔細(xì)分析,從而得出用戶可能最感興趣的建議列表。
谷歌地圖
Google Maps 已幫助通勤者前往新地方,探索新城市并監(jiān)控每日流量。 在 2019 年 6 月上旬,谷歌地圖發(fā)布了一項(xiàng)新功能,使用戶可以監(jiān)控印度 10 個主要城市的巴士旅行時間,以及從印度鐵路局獲得實(shí)時更新。 該功能位于班加羅爾,欽奈,哥印拜陀,德里,海得拉巴,勒克瑙,孟買,浦那和蘇拉特,它利用 Google 的實(shí)時交通數(shù)據(jù)和公交時刻表來計(jì)算準(zhǔn)確的出行時間和延誤。 支持該功能的算法可從總線位置隨時間的順序中學(xué)習(xí)。 該數(shù)據(jù)還與通勤時公交車上的汽車速度結(jié)合在一起。 數(shù)據(jù)還用于捕獲特定街道的獨(dú)特屬性。 研究人員還模擬了圍繞某個區(qū)域彈出查詢的可能性,以使該模型更加健壯和準(zhǔn)確。
Tinder
作為結(jié)識新朋友的全球最受歡迎的應(yīng)用,Tinder 部署了許多學(xué)習(xí)模型,以增加喜歡特定個人資料的人數(shù)。 智能照片功能增加了用戶找到正確匹配項(xiàng)的可能性。 該功能隨機(jī)排序特定用戶的圖片并將其顯示給其他人。 支持該功能的算法分析了向左或向右滑動圖片的頻率。 它使用該知識根據(jù)圖片的受歡迎程度對其重新排序。 隨著越來越多的數(shù)據(jù)收集,該算法的準(zhǔn)確率一直在不斷提高。
Snapchat
Snapchat 使用的過濾器是在圖片和視頻的頂部添加的設(shè)計(jì)疊加層,可以跟蹤面部移動。 這些過濾器是通過計(jì)算機(jī)視覺實(shí)現(xiàn)的。 應(yīng)用使用的算法的第一步是檢測圖像中存在的面部。 它輸出包圍檢測到的面部的框。 然后,它為檢測到的每個臉部標(biāo)記面部標(biāo)志(例如眼睛,鼻子和嘴唇)。 這里的輸出通常是一個包含x
-坐標(biāo)和y
-坐標(biāo)的二維點(diǎn)。 正確檢測到面部和面部特征后,它將使用圖像處理功能在整個面部上正確放置或應(yīng)用過濾器。 該算法使用 Active Shape Model 進(jìn)一步分析了關(guān)鍵的面部特征。 在通過手動標(biāo)記關(guān)鍵面部特征的邊界進(jìn)行訓(xùn)練后,該模型將創(chuàng)建與屏幕上出現(xiàn)的面部對齊的平均面部。 該模型將創(chuàng)建一個網(wǎng)格,以正確放置過濾器并跟蹤其運(yùn)動。
現(xiàn)在,我們來看看 DL 領(lǐng)域的研究領(lǐng)域。
探索移動設(shè)備上 DL 的最新發(fā)展
隨著 DL 和 AI 的復(fù)雜性與移動應(yīng)用的結(jié)合,正在不斷進(jìn)行軟件和硬件優(yōu)化,以在設(shè)備上高效運(yùn)行模型。 讓我們看看其中的一些。
谷歌的 MobileNet
Google 的 MobileNet 于 2017 年推出。它是基于 TensorFlow 的一組移動優(yōu)先計(jì)算機(jī)視覺模型,經(jīng)過精心優(yōu)化以在受限的移動環(huán)境中高效運(yùn)行。 它充當(dāng)復(fù)雜神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu)的準(zhǔn)確率與移動運(yùn)行時性能約束之間的橋梁。 由于這些模型具有在設(shè)備本身上本地運(yùn)行的能力,因此 MobileNet 具有安全性,隱私性和靈活的可訪問性的優(yōu)點(diǎn)。 MobileNet 的兩個最重要的目標(biāo)是在處理計(jì)算機(jī)視覺模型時減小尺寸并降低復(fù)雜性。 MobileNet 的第一個版本提供了低延遲模型,該模型能夠在受限資源下正常工作。 它們可用于分類,檢測,嵌入和分段,支持各種用例。
于 2018 年發(fā)布的 MobileNetV2 是對第一個版本的重大增強(qiáng)。 它可以用于語義分割,對象檢測和分類。 作為 TensorFlow-Slim 圖像分類庫的一部分啟動的 MobileNetV2,可以從 Colaboratory 直接訪問。 也可以在本地下載,使用 Jupyter 進(jìn)行瀏覽,也可以從 TF-Hub 和 GitHub 訪問。 添加到架構(gòu)中的兩個最重要的功能是層之間的線性瓶頸和瓶頸之間的快捷連接。 瓶頸對中間的輸入和輸出進(jìn)行編碼,并且內(nèi)層支持從較低級別的概念轉(zhuǎn)換為較高級別的描述符的功能。 傳統(tǒng)的剩余連接和快捷方式有助于減少訓(xùn)練時間并提高準(zhǔn)確率。 與第一個版本相比,MobileNetV2 更快,更準(zhǔn)確,并且所需的操作和參數(shù)更少。 它非常有效地用于對象檢測和分割以提取特征。
您可以在此處閱讀有關(guān)此研究工作的更多信息。
阿里巴巴移動神經(jīng)網(wǎng)絡(luò)
阿里巴巴移動神經(jīng)網(wǎng)絡(luò)(MNN)是開源的輕量級 DL 推理引擎。 阿里巴巴工程副總裁賈陽清說:“與 TensorFlow 和 Caffe2 等通用框架相比,它既涵蓋訓(xùn)練又包括推理,MNN 專注于推理的加速和優(yōu)化,并解決了模型部署過程中的效率問題。 因此可以在移動端更高效地實(shí)現(xiàn)模型背后的服務(wù),這實(shí)際上與 TensorRT 等服務(wù)器端推理引擎中的思想相符在大型機(jī)器學(xué)習(xí)應(yīng)用中,推理的計(jì)算量通常是 10 倍以上,因此,進(jìn)行推理的優(yōu)化尤為重要?!?/p>
MNN 的主要關(guān)注領(lǐng)域是深度神經(jīng)網(wǎng)絡(luò)(DNN)模型的運(yùn)行和推斷。 它專注于模型的優(yōu)化,轉(zhuǎn)換和推斷。 MNN 已被成功用于阿里巴巴公司的許多移動應(yīng)用中,例如 Mobile Tmall,Mobile Taobao,Fliggy,UC,Qianuu 和 Juhuasuan。 它涵蓋了搜索推薦,短視頻捕獲,直播,資產(chǎn)分配,安全風(fēng)險控制,交互式營銷,按圖像搜索產(chǎn)品以及許多其他實(shí)際場景。 菜鳥呼叫機(jī)柜等物聯(lián)網(wǎng)(IoT)設(shè)備也越來越多地使用技術(shù)。 MNN 具有很高的穩(wěn)定性,每天可以運(yùn)行超過 1 億次。
MNN 具有高度的通用性,并為市場上大多數(shù)流行的框架提供支持,例如 TensorFlow,Caffe 和開放式神經(jīng)網(wǎng)絡(luò)交換(ONNX)。 它與卷積神經(jīng)網(wǎng)絡(luò)(CNN)和循環(huán)神經(jīng)網(wǎng)絡(luò)(RNN)等通用神經(jīng)網(wǎng)絡(luò)兼容。 MNN 輕巧且針對移動設(shè)備進(jìn)行了高度優(yōu)化,并且沒有依賴關(guān)系。 它可以輕松部署到移動設(shè)備和各種嵌入式設(shè)備。 它還通過便攜式操作系統(tǒng)接口(POSIX)支持主要的 Android 和 iOS 移動操作系統(tǒng)以及嵌入式設(shè)備。 MNN 不受任何外部庫的影響,可提供非常高的性能。 它的核心操作通過大量的手寫匯編代碼來實(shí)現(xiàn),以充分利用高級 RISC 機(jī)器(ARM)CPU 的優(yōu)勢。 借助高效的圖像處理模塊(IPM),無需 libyuv 或 OpenCV 即可加速仿射變換和色彩空間變換,MNN 易于使用。
在積極開發(fā)和研究這些產(chǎn)品的同時,現(xiàn)在讓我們看一下將來有望變得越來越重要的一些領(lǐng)域。
探索移動應(yīng)用中 DL 的當(dāng)前研究領(lǐng)域
活躍的研究人員社區(qū)要投入時間和精力,對于任何研究領(lǐng)域的健康發(fā)展至關(guān)重要。 幸運(yùn)的是,DL 在移動設(shè)備上的應(yīng)用引起了全球開發(fā)人員和研究人員的強(qiáng)烈關(guān)注,許多手機(jī)制造商(例如三星,蘋果,Realme 和 Xiaomi)將 DL 直接集成到了系統(tǒng)用戶界面中 (UI)為所有設(shè)備生成。 這極大地提高了模型的運(yùn)行速度,并且通過系統(tǒng)更新定期提高模型的準(zhǔn)確率。
讓我們看一下該領(lǐng)域中一些最受歡迎的研究領(lǐng)域,以及它們是如何發(fā)展的。
DeepFashion
在 2019 年,DeepFashion2 數(shù)據(jù)集由葛玉英,張瑞茂等提出。 該數(shù)據(jù)集是對 DeepFashion 數(shù)據(jù)集的改進(jìn),包括來自賣方和消費(fèi)者的 491,000 張圖像。 數(shù)據(jù)集可識別 801,000 件服裝。 數(shù)據(jù)集中的每個項(xiàng)目都標(biāo)有比例,遮擋,放大,視點(diǎn),類別,樣式,邊界框,密集的界標(biāo)和每個像素的蒙版。
數(shù)據(jù)集在訓(xùn)練集中有 391,000 張圖像,在驗(yàn)證集中有 34,000 張圖像,在測試集中有 67,000 張圖像。 該數(shù)據(jù)集提供了提出更好的模型的可能性,該模型能夠從圖像中識別時裝和不同的服裝。 可以輕松想象此數(shù)據(jù)集可能會導(dǎo)致的應(yīng)用范圍-包括在線商店根據(jù)消費(fèi)者經(jīng)常穿的衣服推薦要購買的產(chǎn)品,以及首選品牌和產(chǎn)品的預(yù)期價格范圍。 僅通過識別他們所穿的服裝和品牌,也有可能識別任何人可能從事的職業(yè)及其財務(wù),宗教和地理細(xì)節(jié)。
您可以在此處閱讀有關(guān) DeepFashion2 數(shù)據(jù)集的更多信息。
自我注意生成對抗網(wǎng)絡(luò)
我們在“第 9 章”,“構(gòu)建圖像超分辨率應(yīng)用”中討論了生成對抗網(wǎng)絡(luò)(GAN)的應(yīng)用,其中我們從低分辨率圖像中生成高分辨率圖像。 GAN 在學(xué)習(xí)模仿藝術(shù)和圖案方面做得相當(dāng)不錯。 但是,在需要記住更長的序列的情況下,以及在序列的多個部分對于生成生成的輸出很重要的情況下,它們無法很好地執(zhí)行。 因此,我們期待 Ian Goodfellow 及其團(tuán)隊(duì)推出的自我注意力 GAN(SAGAN),它們是對圖像生成任務(wù)應(yīng)用注意力驅(qū)動的遠(yuǎn)程依賴建模的 GAN 系統(tǒng)。 該系統(tǒng)在 ImageNet 數(shù)據(jù)集上具有更好的性能,并有望在將來被廣泛采用。
Jason Antic 的 DeOldify 項(xiàng)目是使用 SAGANs 完成的工作的衍生產(chǎn)品。 該項(xiàng)目旨在將色彩帶入舊的圖像和視頻中,從而使它們似乎從來沒有缺少色彩。 以下屏幕快照顯示了 DeOldify 項(xiàng)目的示例:
Dorothea Lange(1936)的《移民母親》。 圖像取自 DeOldify GitHub 存儲庫。 該項(xiàng)目可通過這里進(jìn)行測試和演示。 您可以在這個頁面上了解有關(guān) SAGAN 的更多信息。
圖片動畫
Facebook 是一個流行的社交媒體平臺,具有用于多個平臺的專用應(yīng)用,一直在致力于創(chuàng)建工具,使您可以使用普通的相機(jī)生成 3D 圖像,否則這些相機(jī)只會生成 2D 圖像。 圖像動畫是一項(xiàng)類似的技術(shù),可讓我們將動畫帶入靜態(tài)圖像。 可以想象這種技術(shù)非常令人興奮的用法,人們拍攝自拍照,然后從運(yùn)動庫中進(jìn)行選擇以對其圖像進(jìn)行動畫處理,就好像他們自己在進(jìn)行這些運(yùn)動一樣。
圖像動畫雖然還處于起步階段,但可以成為流行和有趣的應(yīng)用,考慮到采用 Deepfake 技術(shù)的類似應(yīng)用已成功地成為一項(xiàng)業(yè)務(wù)-例如,中國的 Zao 應(yīng)用。
您可以在此處閱讀圖像動畫研究論文。
總結(jié)
在本章中,我們討論了一些最流行的移動應(yīng)用,這些應(yīng)用因其在業(yè)務(wù)產(chǎn)品中最前沿地使用 DL 而著稱,還討論了 DL 影響其增長的方式。 我們還討論了移動應(yīng)用 DL 領(lǐng)域的最新發(fā)展。 最后,我們討論了該領(lǐng)域的一些令人興奮的研究領(lǐng)域,以及它們將來如何發(fā)展成潛在的流行應(yīng)用。 我們相信,到目前為止,您將對如何在移動應(yīng)用上部署 DL 以及如何使用 Flutter 來構(gòu)建可在所有流行的移動平臺上運(yùn)行的跨平臺移動應(yīng)用有一個很好的了解。
我們在本章結(jié)束時希望,您將充分利用本項(xiàng)目系列中介紹的思想和知識,并構(gòu)建出令人敬畏的東西,從而在此技術(shù)領(lǐng)域帶來一場革命。
十一、附錄
計(jì)算機(jī)科學(xué)領(lǐng)域令人興奮的是,它允許多個軟件組件組合在一起并致力于構(gòu)建新的東西。 在這個簡短的附錄中,我們介紹了在移動設(shè)備上進(jìn)行深度學(xué)習(xí)之前需要設(shè)置的工具,軟件和在線服務(wù)。
在本章中,我們將介紹以下主題:
- 在 Cloud VM 上設(shè)置深度學(xué)習(xí)環(huán)境
- 安裝 Dart SDK
- 安裝 Flutter SDK
- 配置 Firebase
- 設(shè)置 Visual Studio(VS)代碼
在 Cloud VM 上設(shè)置深度學(xué)習(xí)環(huán)境
在本節(jié)中,我們將提供有關(guān)如何在 Google Cloud Platform(GCP)計(jì)算引擎虛擬機(jī)(VM)實(shí)例以執(zhí)行深度學(xué)習(xí)。 您也可以輕松地將此處描述的方法擴(kuò)展到其他云平臺。
我們將以快速指南開始,介紹如何創(chuàng)建您的 GCP 帳戶并為其啟用結(jié)算功能。
創(chuàng)建 GCP 帳戶并啟用結(jié)算
要創(chuàng)建 GCP 帳戶,您需要一個 Google 帳戶。 如果您有一個以@gmail.com
結(jié)尾的電子郵件地址或 G Suite 上的帳戶,則您已經(jīng)有一個 Google 帳戶。 否則,您可以通過訪問這里創(chuàng)建一個 Google 帳戶。 登錄到 Google 帳戶后,請執(zhí)行以下步驟:
- 在瀏覽器上訪問這里。
- 接受在彈出窗口中顯示給您的所有條款。
- 您將能夠查看 GCP 控制臺信息中心。 您可以通過閱讀這個頁面上的支持文檔來快速使用此儀表板。
- 在左側(cè)導(dǎo)航菜單上,單擊“計(jì)費(fèi)”以打開計(jì)費(fèi)管理儀表板。 系統(tǒng)將提示您添加一個計(jì)費(fèi)帳戶,如以下屏幕截圖所示:
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-qtGYRynW-1681785128429)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mobi-dl-tflite/img/4246b346-b4a9-4e99-998f-165343832c4e.png)]
- 點(diǎn)擊“添加結(jié)算帳戶”。 如果有資格,您將被重定向到
GCP Free Trial
注冊頁面。 您可以在這個頁面上了解有關(guān)免費(fèi)試用的更多信息。 您應(yīng)該看到類似于以下屏幕截圖的屏幕:
- 根據(jù)需要填寫表格。 創(chuàng)建完帳單后,請返回 GCP 控制臺信息中心。
您已成功創(chuàng)建 GCP 帳戶并為其啟用了結(jié)算功能。 接下來,您將能夠在 GCP 控制臺中創(chuàng)建一個項(xiàng)目并將資源分配給該項(xiàng)目。 我們將在接下來的部分中對此進(jìn)行演示。
創(chuàng)建一個項(xiàng)目和 GCP Compute Engine 實(shí)例
在本部分中,您將在 GCP 帳戶上創(chuàng)建一個項(xiàng)目。 GCP 中的所有資源都封裝在項(xiàng)目下。 項(xiàng)目可能屬于或不屬于組織。 一個組織下可以有多個項(xiàng)目,而一個項(xiàng)目中可能有多個資源。 讓我們開始創(chuàng)建項(xiàng)目,如以下步驟所示:
-
在屏幕的左上方,單擊“選擇項(xiàng)目”下拉菜單。
-
在出現(xiàn)的對話框中,單擊對話框右上方的“新建項(xiàng)目”。
-
您將看到新的項(xiàng)目創(chuàng)建表單,如以下屏幕截圖所示:
- 填寫必要的詳細(xì)信息后,單擊
CREATE
完成創(chuàng)建項(xiàng)目。 創(chuàng)建項(xiàng)目后,將帶您到項(xiàng)目的儀表板。 在這里,您將能夠查看與當(dāng)前所選項(xiàng)目相關(guān)的一些基本日志記錄和監(jiān)視。 您可以在這個頁面上了解有關(guān) GCP 資源組織方式的更多信息。 - 在左側(cè)導(dǎo)航窗格中,單擊
Compute Engine
。 系統(tǒng)將提示您創(chuàng)建一個 VM 實(shí)例。 - 點(diǎn)擊“創(chuàng)建”以顯示 Compute Engine 實(shí)例創(chuàng)建表單。 根據(jù)需要填寫表格。 我們假設(shè)您在創(chuàng)建實(shí)例時選擇了 Ubuntu 18.04 LTS 發(fā)行版。
- 確保在防火墻設(shè)置中啟用對 VM 實(shí)例的 HTTP 和 HTTPS 連接的訪問??,如以下屏幕快照所示:
- 單擊“創(chuàng)建”。 GCP 開始為您配置 VM 實(shí)例。 您將被帶到 VM 實(shí)例管理頁面。 您應(yīng)該在此頁面上看到您的 VM,如以下屏幕截圖所示:
現(xiàn)在,您準(zhǔn)備開始配置此 VM 實(shí)例以執(zhí)行深度學(xué)習(xí)。 我們將在下一部分中對此進(jìn)行介紹。
配置您的 VM 實(shí)例來執(zhí)行深度學(xué)習(xí)
在本節(jié)中,我們將指導(dǎo)您如何安裝包和模塊,以在創(chuàng)建的 VM 實(shí)例上執(zhí)行深度學(xué)習(xí)。 這些包和模塊的安裝說明在您選擇的任何云服務(wù)提供商中都是相似的。
您還可以在本地系統(tǒng)上使用類似的命令,以設(shè)置本地深度學(xué)習(xí)環(huán)境。
首先調(diào)用 VM 的終端:
-
單擊 VM 實(shí)例頁面上的
SSH
按鈕,以啟動到 VM 的終端會話。 -
您應(yīng)該看到終端會話開始,其中包含一些與系統(tǒng)有關(guān)的常規(guī)信息以及上次登錄的詳細(xì)信息,如以下屏幕截圖所示:
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-8qw0xuwj-1681785128430)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mobi-dl-tflite/img/b79b0eac-cebe-4895-9616-87a90fd5d3da.png)]
- 現(xiàn)在,讓我們對該新創(chuàng)建的實(shí)例的包存儲庫執(zhí)行更新:
sudo apt update
- 接下來,我們將在此 VM 上安裝 Anaconda。 Anaconda 是一個受歡迎的包集合,用于使用 Python 執(zhí)行深度學(xué)習(xí)和與數(shù)據(jù)科學(xué)相關(guān)的任務(wù)。 它帶有
conda
包管理器打包在一起,這使得管理系統(tǒng)上安裝的 Python 包的不同版本非常容易。 要安裝它,我們首先需要獲取 Anaconda 安裝程序下載鏈接。 前往這里。 您將轉(zhuǎn)到一個頁面,為您提供要安裝的 Anaconda 版本的選擇,如以下屏幕截圖所示:
- 建議您選擇 Python 3.7 版本。 右鍵單擊“下載”按鈕,然后在菜單中找到允許您復(fù)制鏈接地址的選項(xiàng)。
- 切換到您的 VM 實(shí)例的終端會話。 使用以下命令將占位符文本粘貼到命令中,從而將其替換為您復(fù)制的鏈接,如下所示:
curl -O <link_you_have_copied>
- 前面的命令會將 Anaconda 安裝程序下載到當(dāng)前用戶的主目錄中。 要對其進(jìn)行驗(yàn)證,可以使用
ls
命令。 現(xiàn)在,要將此文件設(shè)置為可執(zhí)行文件,我們將使用以下命令:
chmod +x Anaconda*.sh
- 現(xiàn)在,安裝程序文件可以由您的系統(tǒng)執(zhí)行。 要開始執(zhí)行,請使用以下命令:
./Anaconda*.sh
- 安裝應(yīng)開始。 應(yīng)該顯示一個提示,詢問您是否接受 Anaconda 軟件的許可協(xié)議,如下所示:
- 點(diǎn)擊
Enter
繼續(xù)檢查許可證。 您會看到許可證文件。 - 點(diǎn)擊向下箭頭鍵以閱讀協(xié)議。 輸入
yes
接受許可證。 - 系統(tǒng)將要求您確認(rèn) Anaconda 安裝的位置,如以下屏幕截圖所示:
- 點(diǎn)擊
Enter
確認(rèn)位置。 包提取和安裝將開始。 完成此操作后,系統(tǒng)將詢問您是否要初始化 Anaconda 環(huán)境。 在此處輸入yes
,如下所示:
- 現(xiàn)在,安裝程序?qū)⑼瓿善淙蝿?wù)并退出。 要激活 Anaconda 環(huán)境,請使用以下命令:
source ~/.bashrc
- 您已經(jīng)成功安裝了 Anaconda 環(huán)境并激活了它。 要檢查安裝是否成功,請?jiān)诮K端中輸入以下命令:
python3
如果以下命令的輸出在第二行包含單詞 Anaconda,Inc.,則表明安裝成功。 您可以在以下屏幕截圖中看到它:
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-Yk7ZRtMM-1681785128431)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mobi-dl-tflite/img/6c35a0ee-26db-44c3-831f-a9c4ba78407c.png)]
現(xiàn)在,您可以在此環(huán)境上開始運(yùn)行深度學(xué)習(xí)腳本。 但是,您將來可能希望向此環(huán)境添加更多工具庫,例如 PyTorch 或 TensorFlow 或任何其他包。 由于本書假定讀者熟悉 Python,因此我們不會詳細(xì)討論pip
工具。
現(xiàn)在讓我們看一下如何在 VM 上安裝 TensorFlow。
在 VM 上安裝 TensorFlow
TensorFlow 是執(zhí)行深度學(xué)習(xí)的絕佳框架。
要安裝它,可以使用以下命令:
# TensorFlow 1 with CPU only support
python3 -m pip install tensorflow==1.15# TensorFlow 1 with GPU support
python3 -m pip install tensorflow-gpu==1.15# TensorFlow 2 with CPU only support
python3 -m pip install tensorflow# Tensorflow 2 with GPU support
python3 -m pip install tensorflow-gpu
Python 中另一個經(jīng)常安裝的流行庫是自然語言工具包(NLTK)庫。 我們將在接下來的部分中演示其安裝過程。
在 VM 上安裝 NLTK 并下載包
要在 VM 上安裝 NLTK 并為其下載數(shù)據(jù)包,請執(zhí)行以下步驟:
- 使用
pip
安裝 NLTK:
python3 -m pip install nltk
- NLTK 有幾種不同的數(shù)據(jù)包。 在大多數(shù)情況下,您并不需要全部。 要列出 NLTK 的所有可用數(shù)據(jù)包,請使用以下命令:
python3 -m nltk.downloader
前面命令的輸出將允許您交互式地查看所有可用的包,選擇所需的包,然后下載它們。
- 但是,如果您只希望下載一個包,請使用以下命令:
python3 -m nltk.downloader stopwords
前面的命令將下載 NLTK 的stopwords
數(shù)據(jù)包。 在極少數(shù)情況下,您可能會發(fā)現(xiàn)自己需要或使用 NLTK 中可用的所有數(shù)據(jù)包。
通過這種設(shè)置,您應(yīng)該能夠在云 VM 上運(yùn)行大多數(shù)深度學(xué)習(xí)腳本。
在下一部分中,我們將研究如何在本地系統(tǒng)上安裝 Dart。
安裝 Dart SDK
Dart 是 Google 開發(fā)的一種面向?qū)ο蟮恼Z言。 它用于移動和 Web 應(yīng)用開發(fā)。 Flutter 是用 Dart 構(gòu)建的。 Dart 具有即時(JIT)開發(fā)周期,該狀態(tài)與有狀態(tài)的熱重載兼容,并且具有提前編譯的功能,可以快速啟動并提供可預(yù)測的性能,這使其成為了可能。 適用于 Flutter。
以下各節(jié)討論如何在 Windows,macOS 和 Linux 上安裝 Dart。
Windows
在 Windows 中安裝 Dart 的最簡單方法是使用 Chocolatey。 只需在終端中運(yùn)行以下命令:
C:\> choco install dart-sdk
接下來,我們將研究如何在 Mac 系統(tǒng)上安裝 Dart。
MacOS
要在 macOS 上安裝 Dart,請執(zhí)行以下步驟:
- 通過在終端中運(yùn)行以下命令來安裝 Homebrew:
$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
- 運(yùn)行以下命令以安裝 Dart:
$brew tap dart-lang/dart
$brew install dart
接下來,我們將研究如何在 Linux 系統(tǒng)上安裝 Dart。
Linux
Dart SDK 可以如下安裝在 Linux 中:
- 執(zhí)行以下一次性設(shè)置:
$sudo apt-get update
$sudo apt-get install apt-transport-https
$sudo sh -c 'wget -qO- https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -'
$sudo sh -c 'wget -qO- https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list > /etc/apt/sources.list.d/dart_stable.list'
- 安裝穩(wěn)定版本:
$sudo apt-get update
$sudo apt-get install dart
接下來,我們將研究如何在本地計(jì)算機(jī)上安裝 Flutter SDK。
安裝 Flutter SDK
Flutter 是 Google 的一個工具包,用于使用單個代碼庫構(gòu)建本地編譯的 Android,iOS 和 Web 應(yīng)用。 Flutter 具有熱重載的快速開發(fā),易于構(gòu)建的表達(dá)性 UI 和本機(jī)性能等功能,這些都使 Flutter 成為應(yīng)用開發(fā)人員的首選。
以下各節(jié)討論如何在 Windows,macOS 和 Linux 上安裝 Flutter SDK。
Windows
以下步驟詳細(xì)概述了如何在 Windows 上安裝 Flutter:
- 從這里下載最新的 Flutter SDK 穩(wěn)定版本。
- 解壓縮 ZIP 文件夾,并導(dǎo)航到要安裝 Flutter SDK 的目錄,以放置
flutter
文件夾。
避免將flutter
放在可能需要特殊特權(quán)的目錄中,例如C:\Program Files\
。
- 在“開始”搜索欄中輸入
env
,然后選擇“編輯環(huán)境變量”。 - 使用
;
作為分隔符,將flutter/bin
的完整路徑附加到用戶變量下的路徑。
如果缺少Path
條目,只需創(chuàng)建一個新的Path
變量并將path
設(shè)置為flutter/bin
作為其值。
- 在終端中運(yùn)行
flutter doctor
。
flutter doctor
分析整個 Flutter 的安裝,以檢查是否需要更多工具才能在計(jì)算機(jī)上成功運(yùn)行 Flutter。
接下來,我們將研究如何在 Mac 系統(tǒng)上安裝 Flutter。
MacOS
Flutter 可以如下安裝在 macOS 上:
- 從這里下載最新的穩(wěn)定 SDK。
- 將下載的 ZIP 文件夾解壓縮到合適的位置,如下所示:
$cd ~/
$unzip ~/Downloads/flutter_macos_v1.9.1+hotfix.6-stable.zip
- 將
flutter
工具添加到路徑變量:$ export PATH=
pwd/flutter/bin:$PATH
。 - 打開
bash_profile
以永久更新PATH
:
$cd ~
$nano .bash_profile
- 將以下行添加到
bash_profile
:
$export PATH=$HOME/flutter/bin:$PATH
- 運(yùn)行
flutter doctor
。
Linux
以下步驟概述了如何在 Linux 上安裝 Flutter:
- 從這里下載 SDK 的最新穩(wěn)定版本。
- 將文件提取到合適的位置:
$cd ~/development$tar xf ~/Downloads/flutter_linux_v1.9.1+hotfix.6-stable.tar.xz
- 將
flutter
添加到path
變量中:
$export PATH="$PATH:`pwd`/flutter/bin"
- 運(yùn)行
flutter doctor
。
接下來,我們將研究如何配置 Firebase 以提供 ML Kit 和自定義模型。
配置 Firebase
Firebase 提供了可促進(jìn)應(yīng)用開發(fā)并幫助支持大量用戶的工具。 Firebase 可以輕松用于 Android,iOS 和 Web 應(yīng)用。 Firebase 提供的產(chǎn)品(例如 Cloud Firestore,ML Kit,Cloud Functions,Authentication,Crashlytics,Performance Monitoring,Cloud Messaging 和 Dynamic Links)有助于構(gòu)建應(yīng)用,從而在不斷發(fā)展的業(yè)務(wù)中提高應(yīng)用質(zhì)量。
要集成 Firebase 項(xiàng)目,您需要創(chuàng)建一個 Firebase 項(xiàng)目并將其集成到您的 Android 或 iOS 應(yīng)用中。 以下各節(jié)討論如何創(chuàng)建 Firebase 項(xiàng)目并將其集成到 Android 和 iOS 項(xiàng)目中。
創(chuàng)建 Firebase 項(xiàng)目
首先,我們需要創(chuàng)建一個 Firebase 項(xiàng)目并將其鏈接到我們的 Android 和 iOS 項(xiàng)目。 此鏈接有助于我們利用 Firebase 提供的功能。
要創(chuàng)建 Firebase 項(xiàng)目,請執(zhí)行以下步驟:
- 通過這里訪問 Firebase 控制臺。
- 單擊“添加項(xiàng)目”以添加新的 Firebase 項(xiàng)目:
- 為您的項(xiàng)目提供一個名稱:
- 根據(jù)您的要求啟用/禁用 Google Analytics(分析)。 通常建議您保持啟用狀態(tài)。
Google Analytics 是一種免費(fèi)且不受限制的分析解決方案,可在 Firebase Crashlytics,Cloud Messaging,應(yīng)用內(nèi)消息傳遞,遠(yuǎn)程配置,A/B 測試,預(yù)測和 Cloud Functions 中實(shí)現(xiàn)目標(biāo)定位,報告等功能。
- 如果您選擇 Firebase Analytics,則還需要選擇一個帳戶:
在 Firebase 控制臺上創(chuàng)建項(xiàng)目后,您將需要分別為 Android 和 iOS 平臺進(jìn)行配置。
配置 Android 項(xiàng)目
以下步驟討論了如何配置 Android 項(xiàng)目以支持 Firebase:
- 導(dǎo)航到 Firebase 控制臺上的應(yīng)用。 在項(xiàng)目概述頁面的中心,單擊 Android 圖標(biāo)以啟動工作流程設(shè)置:
- 添加包名稱以在 Firebase 控制臺上注冊該應(yīng)用。 此處填寫的包名稱應(yīng)與您的應(yīng)用的包名稱匹配。 此處提供的包名稱用作標(biāo)識的唯一密鑰:
此外,您可以提供昵稱和調(diào)試簽名證書 SHA-1。
- 下載
google-services.json
文件并將其放在app
文件夾中:
google-services.json
文件存儲開發(fā)人員憑據(jù)和配置設(shè)置,并充當(dāng) Firebase 項(xiàng)目和 Android 項(xiàng)目之間的橋梁。
- 用于 Gradle 的 Google 服務(wù)插件會加載您剛剛下載的
google-services.json
文件。 項(xiàng)目級別的build.gradle
(<project>/build.gradle
)應(yīng)該進(jìn)行如下修改,以使用該插件:
buildscript {repositories {// Check that you have the following line (if not, add it):google() // Google's Maven repository}dependencies {...// Add this lineclasspath 'com.google.gms:google-services:4.3.3'}
}allprojects {...repositories {// Check that you have the following line (if not, add it):google() // Google's Maven repository...}
}
- 這是應(yīng)用級別的
build.gradle
(<p
roject>/<app-module>build.gradle
):
apply plugin: 'com.android.application'
// Add this line
apply plugin: 'com.google.gms.google-services'dependencies {// add SDKs for desired Firebase products// https://firebase.google.com/docs/android/setup#available-libraries
}
現(xiàn)在,您都可以在 Android 項(xiàng)目中使用 Firebase。
配置 iOS 項(xiàng)目
以下步驟演示了如何配置 iOS 項(xiàng)目以支持 Firebase:
- 導(dǎo)航到 Firebase 控制臺上的應(yīng)用。 在項(xiàng)目概述頁面的中心,單擊 iOS 圖標(biāo)以啟動工作流程設(shè)置:
- 添加 iOS 捆綁包 ID 名稱,以在 Firebase 控制臺上注冊該應(yīng)用。 您可以在“常規(guī)”選項(xiàng)卡中的捆綁包標(biāo)識符中找到應(yīng)用主要目標(biāo)的 Xcode。 它用作標(biāo)識的唯一密鑰:
此外,您可以提供昵稱和 App Store ID。
- 下載
GoogleService-Info.plist
文件:
- 將剛剛下載的
GoogleService-Info.plist
文件移到 Xcode 項(xiàng)目的根目錄中,并將其添加到所有目標(biāo)中。
Google 服務(wù)使用 CocoaPods 來安裝和管理依賴項(xiàng)。
- 打開一個終端窗口,然后導(dǎo)航到您的應(yīng)用的 Xcode 項(xiàng)目的位置。 如果沒有,請?jiān)诖宋募A中創(chuàng)建一個 Podfile:
pod init
- 打開您的 Podfile 并添加以下內(nèi)容:
# add pods for desired Firebase products # https://firebase.google.com/docs/ios/setup#available-pods
- 保存文件并運(yùn)行:
pod install
這將為您的應(yīng)用創(chuàng)建一個.xcworkspace
文件。 使用此文件進(jìn)行應(yīng)用的所有將來開發(fā)。
- 要在應(yīng)用啟動時連接到 Firebase,請將以下初始化代碼添加到主
AppDelegate
類中:
import UIKit
import Firebase@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {var window: UIWindow?func application(_ application: UIApplication,didFinishLaunchingWithOptions launchOptions:[UIApplicationLaunchOptionsKey: Any]?) -> Bool {FirebaseApp.configure()return true}
}
現(xiàn)在,您都可以在 Android 項(xiàng)目中使用 Firebase。
設(shè)置 VS 代碼
Visual Studio(VS)Code 是由 Microsoft 開發(fā)的輕型代碼編輯器。 它的簡單性和廣泛的插件存儲庫使其成為開發(fā)人員的便捷工具。 憑借其 Dart 和 Flutter 插件,以及應(yīng)用執(zhí)行和調(diào)試支持,Flutter 應(yīng)用非常易于開發(fā)。
在接下來的部分中,我們將演示如何設(shè)置 VS Code 以開發(fā) Flutter 應(yīng)用。 我們將從這里下載最新版本的 VS Code 開始。
安裝 Flutter 和 Dart 插件
首先,我們需要在 VS Code 上安裝 Flutter 和 Dart 插件。
可以按照以下步驟進(jìn)行:
- 在計(jì)算機(jī)上加載 VS Code。
- 導(dǎo)航到“查看 | 命令面板”。
- 開始輸入
install
,然后選擇擴(kuò)展:安裝擴(kuò)展。 - 在擴(kuò)展搜索字段中鍵入
flutter
,從列表中選擇 Flutter,然后單擊安裝。 這還將安裝所需的 Dart 插件。 - 或者,您可以導(dǎo)航到側(cè)欄來安裝和搜索擴(kuò)展:
成功安裝 Flutter 和 Dart 擴(kuò)展后,我們需要驗(yàn)證設(shè)置。 下一節(jié)將對此進(jìn)行描述。
用 Flutter Doctor 驗(yàn)證設(shè)置
通常建議您驗(yàn)證設(shè)置以確保一切正常。
Flutter 安裝可以通過以下方式驗(yàn)證:
- 導(dǎo)航到“查看 | 命令面板”。
- 輸入
doctor
,然后選擇Flutter: Run Flutter Doctor
。 - 查看“輸出”窗格中的輸出。 輸出中列出了所有錯誤或缺少庫。
- 另外,您可以在終端上運(yùn)行
flutter doctor
來檢查一切是否正常:
上面的屏幕快照顯示,盡管 Flutter 很好用,但其他一些相關(guān)的配置卻丟失了。 在這種情況下,您可能需要安裝所有支持軟件并重新運(yùn)行flutter doctor
以分析設(shè)置。
在 VS Code 上成功設(shè)置 Flutter 之后,我們可以繼續(xù)創(chuàng)建我們的第一個 Flutter 應(yīng)用。
創(chuàng)建第一個 Flutter 應(yīng)用
創(chuàng)建第一個 Flutter 應(yīng)用非常簡單。 執(zhí)行以下步驟:
-
導(dǎo)航到“查看 | 命令面板”。
-
開始輸入
flutter
,然后選擇Flutter: New Project
。 -
輸入項(xiàng)目名稱,例如
my_sample_app
。 -
點(diǎn)擊
Enter
。 -
創(chuàng)建或選擇新項(xiàng)目文件夾的父目錄。
-
等待項(xiàng)目創(chuàng)建完成,然后顯示
main.dart
文件。
有關(guān)更多詳細(xì)信息,請參閱這個頁面上的文檔。
在下一節(jié)中,我們將討論如何運(yùn)行您的第一個 Flutter 應(yīng)用。
運(yùn)行應(yīng)用
一個新的 Flutter 項(xiàng)目的創(chuàng)建帶有一個模板代碼,我們可以直接在移動設(shè)備上運(yùn)行它。 創(chuàng)建第一個模板應(yīng)用后,可以嘗試如下運(yùn)行它:
- 導(dǎo)航至“VS Code”狀態(tài)欄(即窗口底部的藍(lán)色欄):
- 從設(shè)備選擇器區(qū)域中選擇您喜歡的設(shè)備:
- 如果沒有可用的設(shè)備,并且要使用設(shè)備模擬器,請單擊“無設(shè)備”并啟動模擬器:
- 您也可以嘗試設(shè)置用于調(diào)試的真實(shí)設(shè)備。
- 單擊設(shè)置按鈕-位于右上角的齒輪圖標(biāo)齒輪(現(xiàn)已標(biāo)記為紅色或橙色指示器),位于
DEBUG
文本框旁邊,顯示為No Configuration
。 選擇 Flutter,然后選擇調(diào)試配置以創(chuàng)建仿真器(如果已關(guān)閉)或運(yùn)行仿真器或已連接的設(shè)備。 - 導(dǎo)航到“調(diào)試 | 開始調(diào)試”或按
F5
。 - 等待應(yīng)用啟動,進(jìn)度會顯示在
DEBUG CONSOLE
視圖中:
應(yīng)用構(gòu)建完成后,您應(yīng)該在設(shè)備上看到已初始化的應(yīng)用:
在下一節(jié)中,我們將介紹 Flutter 的熱重載功能,該功能有助于快速開發(fā)。
嘗試熱重載
Flutter 提供的快速開發(fā)周期使其適合于時間優(yōu)化的開發(fā)。 它支持有狀態(tài)熱重載,這意味著您可以重載正在運(yùn)行的應(yīng)用的代碼,而不必重新啟動或丟失應(yīng)用狀態(tài)。 熱重裝可以描述為一種方法,您可以通過該方法對應(yīng)用源進(jìn)行更改,告訴命令行工具您要熱重裝,并在幾秒鐘內(nèi)在設(shè)備或仿真器上查看更改。
在 VS Code 中,可以按以下方式執(zhí)行熱重裝:
-
打開
lib/main.dart
。 -
將
You have pushed the button this many times:
字符串更改為You have clicked the button this many times:
。 不要停止您的應(yīng)用。 讓您的應(yīng)用運(yùn)行。 -
保存更改:調(diào)用全部保存,或單擊
Hot Reload
。