ensorFlow 智能移动项目:6~10(5)https://developer.aliyun.com/article/1426912
以下是softmax
函数,它们都很简单:
void softmax(float vals[], int count) { float max = -FLT_MAX; for (int i=0; i<count; i++) { max = fmax(max, vals[i]); } float sum = 0.0; for (int i=0; i<count; i++) { vals[i] = exp(vals[i] - max); sum += vals[i]; } for (int i=0; i<count; i++) { vals[i] /= sum; } }
定义了其他一些辅助函数来测试游戏结束状态:
bool aiWon(int bd[]) { for (int i=0; i<69; i++) { int sum = 0; for (int j=0; j<4; j++) sum += bd[winners[i][j]]; if (sum == 4*AI_PIECE ) return true; } return false; } bool aiLost(int bd[]) { for (int i=0; i<69; i++) { int sum = 0; for (int j=0; j<4; j++) sum += bd[winners[i][j]]; if (sum == 4*HUMAN_PIECE ) return true; } return false; } bool aiDraw(int bd[]) { bool hasZero = false; for (int i=0; i<PIECES_NUM; i++) { if (bd[i] == 0) { hasZero = true; break; } } if (!hasZero) return true; return false; } bool gameEnded(int bd[]) { if (aiWon(bd) || aiLost(bd) || aiDraw(bd)) return true; return false; }
aiWon
和aiLost
函数都使用一个常量数组,该数组定义了所有 69 个可能的获胜位置:
int winners[69][4] = { {0,1,2,3}, {1,2,3,4}, {2,3,4,5}, {3,4,5,6}, {7,8,9,10}, {8,9,10,11}, {9,10,11,12}, {10,11,12,13}, ...... {3,11,19,27}, {2,10,18,26}, {10,18,26,34}, {1,9,17,25}, {9,17,25,33}, {17,25,33,41}, {0,8,16,24}, {8,16,24,32}, {16,24,32,40}, {7,15,23,31}, {15,23,31,39}, {14,22,30,38}};
在触摸事件处理器中,首先确保轮到人了。 然后检查触摸点值是否在面板区域内,根据触摸位置获取点击的列,并更新board
数组和humanMoves
向量:
- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { if (aiTurn) return; UITouch *touch = [touches anyObject]; CGPoint point = [touch locationInView:self.view]; if (point.y < startY || point.y > endY) return; int column = (point.x-startX)/BOARD_COLUMN_WIDTH; for (int i=0; i<6; i++) if (board[35+column-7*i] == 0) { board[35+column-7*i] = HUMAN_PIECE; humanMoves.push_back(35+column-7*i); break; }
其余触摸处理器通过调用createBoardImageInRect
来重绘ImageView
,它使用BezierPath
绘制或重绘棋盘和所有已玩过的棋子,检查游戏状态并在游戏结束时返回结果,或者继续玩游戏,如果没有:
_iv.image = [self createBoardImageInRect:_iv.frame]; aiTurn = true; if (gameEnded(board)) { if (aiWon(board)) _lbl.text = @"AI Won!"; else if (aiLost(board)) _lbl.text = @"You Won!"; else if (aiDraw(board)) _lbl.text = @"Draw"; return; } dispatch_async(dispatch_get_global_queue(0, 0), ^{ std::string result = playGame(withMCTS)); dispatch_async(dispatch_get_main_queue(), ^{ NSString *rslt = [NSString stringWithCString:result.c_str() encoding:[NSString defaultCStringEncoding]]; [_lbl setText:rslt]; _iv.image = [self createBoardImageInRect:_iv.frame]; }); }); }
其余的 iOS 代码全部在createBoardImageInRect
方法中,该方法使用 UIBezierPath
中的moveToPoint
和addLineToPoint
方法绘制面板:
- (UIImage *)createBoardImageInRect:(CGRect)rect { int margin_y = 170; UIGraphicsBeginImageContextWithOptions(CGSizeMake(rect.size.width, rect.size.height), NO, 0.0); UIBezierPath *path = [UIBezierPath bezierPath]; startX = (rect.size.width - 7*BOARD_COLUMN_WIDTH)/2.0; startY = rect.origin.y+margin_y+30; endY = rect.origin.y - margin_y + rect.size.height; for (int i=0; i<8; i++) { CGPoint point = CGPointMake(startX + i * BOARD_COLUMN_WIDTH, startY); [path moveToPoint:point]; point = CGPointMake(startX + i * BOARD_COLUMN_WIDTH, endY); [path addLineToPoint:point]; } CGPoint point = CGPointMake(startX, endY); [path moveToPoint:point]; point = CGPointMake(rect.size.width - startX, endY); [path addLineToPoint:point]; path.lineWidth = BOARD_LINE_WIDTH; [[UIColor blueColor] setStroke]; [path stroke];
bezierPathWithOvalInRect
方法绘制由 AI 和人工移动的所有碎片–根据谁先采取行动,它开始交替绘制碎片,但顺序不同:
int columnPieces[] = {0,0,0,0,0,0,0}; if (aiFirst) { for (int i=0; i<aiMoves.size(); i++) { int action = aiMoves[i]; int column = action % 7; CGRect r = CGRectMake(startX + column * BOARD_COLUMN_WIDTH, endY - BOARD_COLUMN_WIDTH - BOARD_COLUMN_WIDTH * columnPieces[column], BOARD_COLUMN_WIDTH, BOARD_COLUMN_WIDTH); UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:r]; UIColor *color = [UIColor redColor]; [color setFill]; [path fill]; columnPieces[column]++; if (i<humanMoves.size()) { int action = humanMoves[i]; int column = action % 7; CGRect r = CGRectMake(startX + column * BOARD_COLUMN_WIDTH, endY - BOARD_COLUMN_WIDTH - BOARD_COLUMN_WIDTH * columnPieces[column], BOARD_COLUMN_WIDTH, BOARD_COLUMN_WIDTH); UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:r]; UIColor *color = [UIColor yellowColor]; [color setFill]; [path fill]; columnPieces[column]++; } } } else { for (int i=0; i<humanMoves.size(); i++) { int action = humanMoves[i]; int column = action % 7; CGRect r = CGRectMake(startX + column * BOARD_COLUMN_WIDTH, endY - BOARD_COLUMN_WIDTH - BOARD_COLUMN_WIDTH * columnPieces[column], BOARD_COLUMN_WIDTH, BOARD_COLUMN_WIDTH); UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:r]; UIColor *color = [UIColor yellowColor]; [color setFill]; [path fill]; columnPieces[column]++; if (i<aiMoves.size()) { int action = aiMoves[i]; int column = action % 7; CGRect r = CGRectMake(startX + column * BOARD_COLUMN_WIDTH, endY - BOARD_COLUMN_WIDTH - BOARD_COLUMN_WIDTH * columnPieces[column], BOARD_COLUMN_WIDTH, BOARD_COLUMN_WIDTH); UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:r]; UIColor *color = [UIColor redColor]; [color setFill]; [path fill]; columnPieces[column]++; } } } UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image; }
现在运行该应用,您将看到类似于图 10.4 的屏幕:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kOcc3SVn-1681653119041)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/intel-mobi-proj-tf/img/fa949c2a-3114-4a56-9041-9acb069b1ff4.png)]
图 10.4:在 iOS 上玩 Connect4
使用 AI 玩一些游戏,图 10.5 显示了一些可能的最终游戏:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b6oYmCmM-1681653119041)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/intel-mobi-proj-tf/img/1e45df24-e772-462b-aef9-67c8f49bed67.png)]
图 10.5:iOS 上 Connect4 的一些游戏结果
在我们暂停之前,让我们快速看一下使用该模型并玩游戏的 Android 代码。
在 Android 中使用模型玩 Connect4
毫不奇怪,我们不需要像第 7 章,“使用 CNN 和 LSTM 识别绘画”那样使用自定义 Android 库来加载模型。 只需创建一个名称为 AlphaZero 的新 Android Studio 应用,将alphazero19.pb
模型文件复制到新创建的素材资源文件夹,然后将 compile 'org.tensorflow:tensorflow-android:+'
行添加到应用的build.gradle
文件。
我们首先创建一个新类BoardView
,该类扩展了View
并负责绘制游戏板以及 AI 和用户制作的棋子:
public class BoardView extends View { private Path mPathBoard, mPathAIPieces, mPathHumanPieces; private Paint mPaint, mCanvasPaint; private Canvas mCanvas; private Bitmap mBitmap; private MainActivity mActivity; private static final float MARGINX = 20.0f; private static final float MARGINY = 210.0f; private float endY; private float columnWidth; public BoardView(Context context, AttributeSet attrs) { super(context, attrs); mActivity = (MainActivity) context; setPathPaint(); }
我们使用了mPathBoard
,mPathAIPieces
和mPathHumanPieces
这三个Path
实例分别绘制了板子,AI 做出的动作和人类做出的不同颜色的。 。 BoardView
的绘制功能是通过Path
的moveTo
和lineTo
方法以及Canvas
的drawPath
方法在onDraw
方法中实现的:
protected void onDraw(Canvas canvas) { canvas.drawBitmap(mBitmap, 0, 0, mCanvasPaint); columnWidth = (canvas.getWidth() - 2*MARGINX) / 7.0f; for (int i=0; i<8; i++) { float x = MARGINX + i * columnWidth; mPathBoard.moveTo(x, MARGINY); mPathBoard.lineTo(x, canvas.getHeight()-MARGINY); } mPathBoard.moveTo(MARGINX, canvas.getHeight()-MARGINY); mPathBoard.lineTo(MARGINX + 7*columnWidth, canvas.getHeight()- MARGINY); mPaint.setColor(0xFF0000FF); canvas.drawPath(mPathBoard, mPaint);
如果 AI 首先移动,我们开始绘制第一个 AI 移动,然后绘制第一个人类移动(如果有的话),并交替绘制 AI 和人类移动的图形:
endY = canvas.getHeight()-MARGINY; int columnPieces[] = {0,0,0,0,0,0,0}; for (int i=0; i<mActivity.getAIMoves().size(); i++) { int action = mActivity.getAIMoves().get(i); int column = action % 7; float x = MARGINX + column * columnWidth + columnWidth / 2.0f; float y = canvas.getHeight()-MARGINY- columnWidth*columnPieces[column]-columnWidth/2.0f; mPathAIPieces.addCircle(x,y, columnWidth/2, Path.Direction.CW); mPaint.setColor(0xFFFF0000); canvas.drawPath(mPathAIPieces, mPaint); columnPieces[column]++; if (i<mActivity.getHumanMoves().size()) { action = mActivity.getHumanMoves().get(i); column = action % 7; x = MARGINX + column * columnWidth + columnWidth / 2.0f; y = canvas.getHeight()-MARGINY- columnWidth*columnPieces[column]-columnWidth/2.0f; mPathHumanPieces.addCircle(x,y, columnWidth/2, Path.Direction.CW); mPaint.setColor(0xFFFFFF00); canvas.drawPath(mPathHumanPieces, mPaint); columnPieces[column]++; } }
如果人先移动,则将应用类似的绘图代码,如 iOS 代码中一样。 在BoardView
的public boolean onTouchEvent(MotionEvent event)
内部,如果轮到 AI 了,则返回它,我们检查哪一列已被挖掘,并且如果该列还没有被全部六个可能的片断填满,则将新的人工移动添加到humanMoves
MainActivity
的向量,然后重绘视图:
public boolean onTouchEvent(MotionEvent event) { if (mActivity.getAITurn()) return true; float x = event.getX(); float y = event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_MOVE: break; case MotionEvent.ACTION_UP: if (y < MARGINY || y > endY) return true; int column = (int)((x-MARGINX)/columnWidth); for (int i=0; i<6; i++) if (mActivity.getBoard()[35+column-7*i] == 0) { mActivity.getBoard()[35+column-7*i] = MainActivity.HUMAN_PIECE; mActivity.getHumanMoves().add(35+column-7*i); break; } invalidate();
之后,将回合设置为 AI,如果游戏结束则返回。 否则,在人类可以触摸并选择下一步动作之前,让 AI 根据模型的策略返回进行下一步动作,以启动新线程继续玩游戏:
mActivity.setAiTurn(); if (mActivity.gameEnded(mActivity.getBoard())) { if (mActivity.aiWon(mActivity.getBoard())) mActivity.getTextView().setText("AI Won!"); else if (mActivity.aiLost(mActivity.getBoard())) mActivity.getTextView().setText("You Won!"); else if (mActivity.aiDraw(mActivity.getBoard())) mActivity.getTextView().setText("Draw"); return true; } Thread thread = new Thread(mActivity); thread.start(); break; default: return false; } return true; }
UI 的主要布局是在activity_main.xml
中定义的,它由三个 UI 元素组成:TextView
,自定义BoardView
和Button
:
<TextView android:id="@+id/textview" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="" android:textAlignment="center" android:textColor="@color/colorPrimary" android:textSize="24sp" android:textStyle="bold" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.06"/> <com.ailabby.alphazero.BoardView android:id="@+id/boardview" android:layout_width="fill_parent" android:layout_height="fill_parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent"/> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Play" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.94" />
在MainActivity.java
中,首先定义一些常量和字段:
public class MainActivity extends AppCompatActivity implements Runnable { private static final String MODEL_FILE = "file:///android_asset/alphazero19.pb"; private static final String INPUT_NODE = "main_input"; private static final String OUTPUT_NODE1 = "value_head/Tanh"; private static final String OUTPUT_NODE2 = "policy_head/MatMul"; private Button mButton; private BoardView mBoardView; private TextView mTextView; public static final int AI_PIECE = -1; public static final int HUMAN_PIECE = 1; private static final int PIECES_NUM = 42; private Boolean aiFirst = false; private Boolean aiTurn = false; private Vector<Integer> aiMoves = new Vector<>(); private Vector<Integer> humanMoves = new Vector<>(); private int board[] = new int[PIECES_NUM]; private static final HashMap<Integer, String> PIECE_SYMBOL; static { PIECE_SYMBOL = new HashMap<Integer, String>(); PIECE_SYMBOL.put(AI_PIECE, "X"); PIECE_SYMBOL.put(HUMAN_PIECE, "O"); PIECE_SYMBOL.put(0, "-"); } private TensorFlowInferenceInterface mInferenceInterface;
然后像在 iOS 版本的应用中一样定义所有获胜职位:
private final int winners[][] = { {0,1,2,3}, {1,2,3,4}, {2,3,4,5}, {3,4,5,6}, {7,8,9,10}, {8,9,10,11}, {9,10,11,12}, {10,11,12,13}, ... {0,8,16,24}, {8,16,24,32}, {16,24,32,40}, {7,15,23,31}, {15,23,31,39}, {14,22,30,38}};
BoardView
类使用的一些获取器和设置器:
public boolean getAITurn() { return aiTurn; } public boolean getAIFirst() { return aiFirst; } public Vector<Integer> getAIMoves() { return aiMoves; } public Vector<Integer> getHumanMoves() { return humanMoves; } public int[] getBoard() { return board; } public void setAiTurn() { aiTurn = true; }
还有一些助手,它们是 iOS 代码的直接端口,用于检查游戏状态:
public boolean aiWon(int bd[]) { for (int i=0; i<69; i++) { int sum = 0; for (int j=0; j<4; j++) sum += bd[winners[i][j]]; if (sum == 4*AI_PIECE ) return true; } return false; } public boolean aiLost(int bd[]) { for (int i=0; i<69; i++) { int sum = 0; for (int j=0; j<4; j++) sum += bd[winners[i][j]]; if (sum == 4*HUMAN_PIECE ) return true; } return false; } public boolean aiDraw(int bd[]) { boolean hasZero = false; for (int i=0; i<PIECES_NUM; i++) { if (bd[i] == 0) { hasZero = true; break; } } if (!hasZero) return true; return false; } public boolean gameEnded(int[] bd) { if (aiWon(bd) || aiLost(bd) || aiDraw(bd)) return true; return false; }
getAllowedActions
方法(也是 iOS 代码的直接端口)将给定板位置的所有允许的动作设置为actions
向量:
void getAllowedActions(int bd[], Vector<Integer> actions) { for (int i=0; i<PIECES_NUM; i++) { if (i>=PIECES_NUM-7) { if (bd[i] == 0) actions.add(i); } else { if (bd[i] == 0 && bd[i+7] != 0) actions.add(i); } } }
在onCreate
方法中,实例化三个 UI 元素,并设置按钮单击监听器,以便它随机决定谁先采取行动。 当用户想要重玩游戏时,也会点击该按钮,因此我们需要在绘制面板和启动线程进行游戏之前重置aiMoves
和humanMoves
向量:
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mButton = findViewById(R.id.button); mTextView = findViewById(R.id.textview); mBoardView = findViewById(R.id.boardview); mButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mButton.setText("Replay"); mTextView.setText(""); Random rand = new Random(); int n = rand.nextInt(2); aiFirst = (n==0); if (aiFirst) aiTurn = true; else aiTurn = false; if (aiTurn) mTextView.setText("Waiting for AI's move"); else mTextView.setText("Tap the column for your move"); for (int i=0; i<PIECES_NUM; i++) board[i] = 0; aiMoves.clear(); humanMoves.clear(); mBoardView.drawBoard(); Thread thread = new Thread(MainActivity.this); thread.start(); } }); }
线程启动run
方法,该方法进一步调用playGame
方法,首先将板的位置转换为binary
整数数组,以用作模型的输入:
public void run() { final String result = playGame(); runOnUiThread( new Runnable() { @Override public void run() { mBoardView.invalidate(); mTextView.setText(result); } }); } String playGame() { if (!aiTurn) return "Tap the column for your move"; int binary[] = new int[PIECES_NUM*2]; for (int i=0; i<PIECES_NUM; i++) if (board[i] == 1) binary[i] = 1; else binary[i] = 0; for (int i=0; i<PIECES_NUM; i++) if (board[i] == -1) binary[42+i] = 1; else binary[PIECES_NUM+i] = 0;
playGame
方法的其余部分也几乎是 iOS 代码的直接端口,它调用getProbs
方法以使用为所有操作返回的概率值来获取所有允许的操作中的最大概率值, 该模型的策略输出中总共包括 42 个法律和非法的:
float probs[] = new float[PIECES_NUM]; for (int i=0; i<PIECES_NUM; i++) probs[i] = -100.0f; getProbs(binary, probs); int action = -1; float max = 0.0f; for (int i=0; i<PIECES_NUM; i++) { if (probs[i] > max) { max = probs[i]; action = i; } } board[action] = AI_PIECE; printBoard(board); aiMoves.add(action); if (aiWon(board)) return "AI Won!"; else if (aiLost(board)) return "You Won!"; else if (aiDraw(board)) return "Draw"; aiTurn = false; return "Tap the column for your move"; }
如果尚未加载getProbs
方法,则加载模型;使用当前板状态作为输入运行模型;并在调用softmax
以获得真实概率值之前获取输出策略,该值之和对于允许的动作为 1:
void getProbs(int binary[], float probs[]) { if (mInferenceInterface == null) { AssetManager assetManager = getAssets(); mInferenceInterface = new TensorFlowInferenceInterface(assetManager, MODEL_FILE); } float[] floatValues = new float[2`6`7]; for (int i=0; i<2`6`7; i++) { floatValues[i] = binary[i]; } float[] value = new float[1]; float[] policy = new float[42]; mInferenceInterface.feed(INPUT_NODE, floatValues, 1, 2, 6, 7); mInferenceInterface.run(new String[] {OUTPUT_NODE1, OUTPUT_NODE2}, false); mInferenceInterface.fetch(OUTPUT_NODE1, value); mInferenceInterface.fetch(OUTPUT_NODE2, policy); Vector<Integer> actions = new Vector<>(); getAllowedActions(board, actions); for (int action : actions) { probs[action] = policy[action]; } softmax(probs, PIECES_NUM); }
softmax
方法的定义与 iOS 版本中的定义几乎相同:
void softmax(float vals[], int count) { float maxval = -Float.MAX_VALUE; for (int i=0; i<count; i++) { maxval = max(maxval, vals[i]); } float sum = 0.0f; for (int i=0; i<count; i++) { vals[i] = (float)exp(vals[i] - maxval); sum += vals[i]; } for (int i=0; i<count; i++) { vals[i] /= sum; } }
现在,在 Android 虚拟或真实设备上运行该应用并使用该应用进行游戏,您将看到初始屏幕和一些游戏结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aUqRx2QC-1681653119041)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/intel-mobi-proj-tf/img/2d40ca7c-5d3d-4586-9034-2b2ae1c71ecb.png)]
图 10.6:在 Android 上显示游戏板和一些结果
当您使用前面的代码在 iOS 和 Android 上玩游戏时,很快就会发现该模型返回的策略并不强大-主要原因是 MCTS 没有出现在这里,由于范围限制,不会与深度神经网络模型一起使用。 强烈建议您自己研究和实现 MCTS,或者在源代码存储库中使用我们的实现作为参考。 您还应该将网络模型和 MCTS 应用于您感兴趣的其他游戏-毕竟,AlphaZero 使用了通用 MCTS 和无领域知识的自我强化学习,从而使超人学习轻松移植到其他问题领域。 通过将 MCTS 与深度神经网络模型结合,您可以实现 AlphaZero 所做的事情。
总结
在本章中,我们介绍了 AlphaZero 的惊人世界,这是 DeepMind 截至 2017 年 12 月的最新和最大成就。我们向您展示了如何使用功能强大的 Keras API 和 TensorFlow 后端为 Connect4 训练类似 AlphaZero 的模型,以及如何测试并可能改善这种模型。 然后,我们冻结了该模型,并详细介绍了如何构建 iOS 和 Android 应用以使用该模型,以及如何使用基于模型的 AI 玩 Connect4。 尚不能完全击败人类象棋或 GO 冠军的确切 AlphaZero 模型,但我们希望本章为您提供扎实的基础,并激发您继续进行工作,以复制 AlphaZero 最初所做的工作并将其进一步扩展到其他问题领域。 这将需要很多努力,但完全值得。
如果最新的 AI 进展(例如 AlphaZero)使您兴奋不已,那么您还可能会发现由 TensorFlow 驱动的最新移动平台解决方案或工具包令人兴奋。 如我们在第 1 章“移动 TensorFlow 入门”中提到的,TensorFlow Lite 是 TensorFlow Mobile 的替代解决方案,我们在前面的所有章节中都有介绍。 根据 Google 的说法,TensorFlow Lite 将成为 TensorFlow 在移动设备上的未来,尽管在此时和可预见的将来,TensorFlow Mobile 仍应用于生产场合。
虽然 TensorFlow Lite 在 iOS 和 Android 上均可使用,但在 Android 设备上运行时,它也可以利用 Android Neural Networks API 进行硬件加速。 另一方面,iOS 开发人员可以利用 Core ML, Apple 针对 iOS 11 或更高版本的最新机器学习框架,该框架支持运行许多强大的预训练深度学习模型,以及使用经典的机器学习算法和 Keras,以优化的方式在设备上以最小的应用二进制文件大小运行。 在下一章中,我们将介绍如何在 iOS 和 Android 应用中使用 TensorFlow Lite 和 Core ML。