人工智能 java 坦克机器人系列: 强化学习_人工智能 Java 坦克機器人系列: 強化學習...

級別:中級

2006

7

月13日

本文中,我們將使用強化學習來實現一個機器人。使用強化學習能創建一個自適應的戰斗機器人。這個機器人能在戰斗中根據環境取得最好的策略,並盡力使戰斗行為最佳。並在此過程中不斷學習以完善自身不足。

Robocode是IBM開發的Java戰斗機器人平台,游戲者可以在平台上設計一個Java坦克。每個坦克有個從戰場上收集信息的感應器,並且它們還有一個執行動作的傳動器。其規則和原理類似於現實中的坦克戰斗。其融合了機器學習、物理、數學等知識,是研究人工智能的很好工具。

在Robocode坦克程序中,很多愛好者喜歡設計一些策略與移動模式,讓自己的坦克機器人能更好的贏得戰斗。但是由於Robocode環境時刻在變化,手寫的代碼只能對已知的環境做一些預測,機器人不能根據環境的變化而自我學習和改善。本文中,將用強化學習實現一個機器人。使用強化學習能創建一個自適應的戰斗機器人。這個機器人能在戰斗中根據環境取得最好的策略,並盡力使戰斗行為最佳。並在此過程中不斷學習以完善自身不足。

強化學習(reinforcement learning)是人工智能中策略學習的一種,是一種重要的機器學習方法,又稱再勵學習、評價學習.是從動物學習、參數擾動自適應控制等理論發展而來.

強化學習一詞來自於行為心理學,這一理論把行為學習看成是反復試驗的過程,從而把動態環境狀態映射成相應的動作。它通過不斷嘗試錯誤,從環境中得到獎懲的方法來自主學習到不同狀態下哪些動作具有最大的價值,從而發現或逼近能夠得到最大獎勵的策略。它類似於傳統經驗中的“吃一塹長一智”。

考慮建造一個可學習的機器人,該機器人(或agent)有一些傳感器可以觀察其環境的狀態(state)並能做出一組動作(action)來適應這些狀態。比如:一個移動的機器人有攝像頭等傳感器來感知狀態,並可以做"前進","后退"等動作。學習的任務是獲得一個控制策略(policy),以選擇能達到的目的的行為。

強化學習基本原理也是基於上面的思想:如果Agent的某個行為策略導致環境正的獎賞(強化信號),那么Agent以后產生這個行為策略的趨勢便會加強。Agent的目標可被定義為一個獎賞或回報函數(reward),它對Agent從不同狀態中選取的不同動作賦予一個數字值,即立即支付(immediate payoff)。比如機器人尋找箱子中的回報函數:對能找到的狀態-動作賦予正回報,對其他狀態動作賦予零或負回報。機器人的任務執行一系列動作,觀察結果,再學習控制策略,我們希望的控制策略是在任何初始離散狀態中選擇動作,使Agent隨時間累積中發現最優策略以使期望的折扣獎賞(回報)和最大。

如圖描述:Agent選擇一個動作(action)用於環境,環境(Enviironment)接受該動作后狀態(state)發生變化,同時產生一個強化信號(獎賞reward)反饋給Agent,Agent根據強化信號和環境當前狀態再選擇下一個動作,選擇的原則是使受到正強化(獎)的概率增大。

Q學習(Q-learning)

增強學習要解決的問題:一個能夠感知環境的自治Agent,怎樣通過學習選擇達到其目標的最優動作。這樣一個Agent在任意的環境中如何學到最優策略是我們要重點考慮的對象,下面介紹的稱為Q學習的算法,就是其中比較好的一種強化學習算法,它可從有延遲的回報中獲取最優控制策略。

Q學習是強化學習的一種形式,機器人在任意的環境中直接學習最優策略很難,因為訓練數據中沒有提供形式的訓練樣例。而通過學習一個定義在狀態和動作上的數值評估函數,然后以此評估函數的形式實現最優策略將會使過程變得容易。

我們在Q學習中把Q表示在狀態s進行t動作的預期值;s是狀態向量;a是動作向量;r是獲得的立即回報;為折算因子。則估計函數Q(s,a)被定義為:它的值是從狀態s開始並使用a作為第一個動作時可獲得的最大期望折算積累回報。也就是說Q值是從狀態s執行動作a的立即回報加上遵循最穩定最優策略的值(用折算)。公式如下:

Q(s,a)=r(s,a)+

maxQ(s'+a')

我們用過程來表述Q學習算法如下:

1.對每個s,a初始化表項Q(s,a)

2.觀察當前狀態s,一直重復做:

a.選擇一個動作a並執行它

b.接收到立即回報r

c.觀察新狀態s'

d.對Q(s,a)按照下式更新表項Q(s,a)

maxQ(s'+a')

e.s

其中(

)是折算因子,為一常量。

為了說明這些概念,我們用一些簡單的格狀確定世界來模擬環境。在這個環境下所有的動作轉換除了導向狀態G外,都被定義為0,agent一進入狀態G,可選動作只能處在該狀態中。圖中方格表示agent的6種可能狀態或位置,每個箭頭代表每個不同的動作。如果agent執行相應狀態動作可收到立即回報r(s,a),V?(s)為最優策略的值函數,即從最初狀態s到獲得的折算積累回報。此處立即回報函數把進入目標狀態G的回報賦予100,其他為0,V?(s)和Q(s,a)值來源於r(s,a),以及折算因子=0.9.

定義了狀態、動作、立即回報和折算率,我們根據計算就能得出最優策略圖4和它的值函數V?(s).該策略把agent以最短路徑導向狀態G。圖3顯示了每個狀態的V?值,例如,圖3中下方的狀態中最優策略使agent向右移動,得到為0的立即回報,然后向上,生成100的立即回報,,此狀態的折算回報計算為:

0+

100+

+

+…=90,即中上方的V?值為90

Q學習的優點是即使在學習不具有其動作怎樣影響環境的先驗知識情況下,此算法仍可應用.

強化學習主要應用在三個方面:在機器人中的應用,強化學習最適合、也是應用最多的。Wnfriedllg采用強化學習來使六足昆蟲機器人學會六條腿的協調動作。Sebastian Thurn采用神經網絡結合強化學習方式使機器人通過學習能夠到達室內環境中的目標;在游戲比賽中的應用,在這方面,最早的應用例子是Samuel的下棋程序;在控制系統中的應用,強化學習在控制中的應用的典型實例,就是倒擺控制系統.當倒擺保持平衡時,得到獎勵,倒擺失敗時,得到懲罰,控制器通過自身的學習,最終得到最優的控制動作。

從上面的強化學習原理中我們知道,要實現強化學習我們必須知道狀態、動作以及處理這些狀態和動作的Q函數,在對狀態和動作的反復實驗當中,我們還要給出動作的獎賞、設定學習率、折算率等參數。最后我們還要利用Q學習算法把上面提到的參數組合進行最優化,最終得到自己想要的值。下面我們就從robocode來分析上面提到的參數和強化學習的實現過程。

Robocode是根據戰斗環境模擬而來,所以在此環境中存在很多種狀態,不同的狀態對機器人會產生不同的影響。這些狀態我們都可通過Robocode的函數調用得到,如下表列出了Robocode中能得到的部分狀態值。

如此多的狀態如果全部放到強化學習中,會耗費強化學習很多時間。而且像getBattleFieldHeight()、getBattleFieldWidth()得到場地高和寬,getGunCoolingRate()炮管冷卻率,這些狀態值與學習無關聯,而有些狀態是與不同的動作相結合的,如果使用不當,甚至會達不到應有的效果。所以如何確定和選擇狀態是很關鍵的問題。根據戰斗經驗與測試數據,我們把Robocode機器人的狀態分為五個屬性,並以類state來封裝所有的這五個屬性。最后我們還給出了在我們的強化學習中沒有應用到但同樣重要的一些狀態,大家有興趣可補充進自己的強化學習算法當中。

強化學習應用到Robocode遇到的最大挑戰是:強化學習適合離散空間求解,而Robocode的環境卻是連續的。如上表中Robocode的狀態中,機器人的方向角(二維矢量),兩個機器人的相對角(二維矢量),兩個機器人的距離(二維矢量),場地坐標,機器人的x,y坐標等等,輸出是一組動作序列,這些都是連續量,若對所有變量進行離散化必然帶來維數災難。所以,我們必須離散化輸入狀態。以適合強化學習算法的應用。注意Robocode中的角度、距離等狀態都有一些特點:角度是分四個方位,而且是0到360度之間的四個區間值,而兩兩機器人之間的安全距離一般是維持在30象素左右。根據Robocode這些特點和強化學習原理,我們分別以4,30等區間值來轉換狀態值為離散點。

1.機器人的絕對方向(Heading)

知已知彼,百戰不怠,在開始戰斗前知道敵人和自己的方向很重要。在Robocode中要想得到敵人的方向,我們首先要知道Robocode的坐標系統。Robocode坐標系統是一個標准的笛卡爾坐標,戰場地圖的左下角坐標為(0,0),右下角為(地圖寬,0),左上角為(0,地圖高),右上角為(地圖寬,地圖高),如下圖所示:

從圖上我們知道了如果機器人處於場地中央則面向場地水平向上為0度方向,按順時針轉動,水平向右為90度方向,水平向左為270度方向,水平向下為360度方向。也就是說0 <= heading < 360。此角度值是一個絕對值。在Robocode中我們通過調用getHeight()函數能得到想要的機器人當前方向。機器人處於場地什么位置,就可對照下圖得到其對應的角度。

為了保存角度到Q表中,我們把得到的連續值轉換為離散的值。按照角的大小和方位我們把heading分成范圍為0-3的四份,如下圖:

在代碼我們直接用360除以4得到不同的離散方向值。

public static final int NumHeading = 4;

double angle = 360 / NumHeading;

然后直接通過下面的表達式求得新的方向角

double newHeading = heading + angle / 2;

return (int)(newHeading / angle);

2.機器人的相對角(Bearing)

上面我們知道了機器人的絕對度,但是戰斗都是存在於兩個機器人之間,所以這里我們要了解Robocode的第二個狀態值相對角,顧名思義,它就是某一機器人相對另一機器人的角度,是針對兩個機器人而言的。如下圖所示:

圖1顯示了機器人相對於自己的bearing角度。而圖2顯示了機器人r2相對於r1的b = 60度。由此可知,-180 < bearing <= 180。如圖相對度在機器人左邊為正,右邊為負。在Robocode中我們可以通過函數getBearing()得到相對角。相對角是個很有用的函數,通過確定敵人相對度,我們能得到最佳的移動位置。

和上面同樣的道理我們把bearing分成0-3的四份,在代碼我們直接用PI值除以4得到不同的離散相對角值

public static final int NumTargetBearing = 4;

double angle = PIx2 / NumTargetBearing;

然后直接通過下面的表達式求得新的相對角

double newBearing = bearing + angle / 2;

return (int)(newBearing / angle);

3.目標距離(distance)

distance即兩個機器人之間長度,即自己機器人中心點到敵人機器人中心點連線長。距離是以像素(Pixels)點為單位,如何確定其離散值的大小以方便保存到Q表中有一定的難度。在此我們根據機器人本身的大小(30為安全距離)以及場地大小設置,我們把距離分為0-19的20個離散值。每個離散值分為30的倍數,如下表:

在代碼我們直接用距離值除以30得到不同的離散值

public static final int NumTargetDistance = 20;

int distance = (int)(value / 30.0);

然后直接通過下面的表達式求得新的距離

if (distance > NumTargetDistance - 1)

distance = NumTargetDistance - 1;

return distance;

4.Hit Wall(撞牆)

當機器人撞擊牆時,能量會發生改變,這時"Hit wall"事件會觸發,我們就此狀態定義為"Hit wall",而機器人根據這個事件能做相應的動作。撞牆狀態比較簡單,只有撞也沒有撞,所以我們在此用兩個離散值來表示這一狀態,0表示沒有撞牆,狀態沒有發生。1表示撞牆狀態發生。通過Robocode的ohHitWall事件我們能得到這個狀態。

5.Hit by Bullet(子彈相撞)

當機器人被子彈擊中時,能量會發生改變,這時"Hit by Bullet"事件會觸發,我們就此狀態定義為"Hit by Bullet",而機器人根據這個事件也能做出相應的動作。同樣的我們在此用兩個離散值來表示這一狀態,0表示沒有撞牆,狀態沒有發生。1表示撞牆狀態發生。通過onHitByBullet()事件我們能得到這個狀態。

因為Hit wall和Hit by bullet本身都是Robocode的事件函數,所以在直接把離散值寫入Robocode本身的事件當中。

根據上面五個狀態的組合,我們可得到共1280 (4 x 20 x 4 x 2 x 2)可能存在的狀態。在robocode中還有其他一些狀態:比如兩個機器人相撞的HitRobotEvent事件狀態,團隊中的消息接收MessageEvent事件狀態,還有機器人的x,y坐標狀態,機器人在戰地中間的坐標和戰地邊緣的坐標都會對狀態產生影響。如果有興趣大家可以試一試別的狀態處理。

動作集(Action)

Robocode的動作相對是比較復雜的,而且涉及到炮管、雷達和機器人本身的移動。為了簡化操作,在此處我們只定義了機器人自身的移動動作集:移動和轉動。在Robocode中最基本的移動就是前進和后退,轉動就是向左或向右。同狀態一樣,我們把Action中的值進化離散化以保存到Q表中。如下代碼:

public static final int RobotAhead = 0;

public static final int RobotBack = 1;

public static final int RobotAheadTurnLeft = 2;

public static final int RobotAheadTurnRight= 3;

public static final int RobotBackTurnLeft = 4;

public static final int RobotBackTurnRight= 5;

我們知道了狀態、動作,就要面臨如何根據狀態來選擇機器人的動作。選擇的動作不僅影響立即強化值,而且影響環境下一時刻的狀態及最終的強化值。所以好的選擇方法很重要。在強化學習中通常使用概率來選擇動作,對於狀態S,做不同Q值的動作時賦予不同的概率,高值得到高概率,低值得到低概率,所有動作的概率都非0。如下方法:

P (a | s) = e^Q(s, a) / sum (e^Q(s, ai))

其中P (a | s)為機器人在狀態s時選擇的動作a的概率,e為一常量大於0。在代碼中我們通過getQValue得到當前Q(s,a)值,並利用上面的公式在所有的Q值中選擇出最優的動作。如下代碼,其中ExploitationRate設定為1.

for (int i = 0; i < value.length; i++)

{

qValue = table.getQValue(state, i);

value[i] = Math.exp(ExploitationRate * qValue);

sum += value[i];

}

上面我們定義了狀態/動作值,而且從強化學習概念我們知道狀態/動作值都是以對的方式存在。我們把這種值叫Q(s,a)值。學習都是在前一個Q值的基礎上對新的值進行判斷和完善。所以在強化學習中定義了Q表用以保存所有收集的Q值。由於狀態和動作是成對存在,二維數組是保存Q值的最佳工具,如下代碼。

private double[][] table;

table = new double[State.NumStates][Action.NumRobotActions];

由代碼可知表值大小決定於狀態和動作的數量,這兩者數量越多,表越大。Robocode是以回合制的方式進行戰斗,要想在每個回合都能利用到原始Q值,文件是最好的通訊工具,通過java文件流我們定義輸入(input)和輸出(output)函數來保存數據到文本文件。這樣在每個回合中,我們都能對原始數據進行分析處理,並不斷把新的數據寫入表中。

Q表中在強化學習中是很重要的概念,它不僅保存了所有Q值,同時也定義了對這些Q值進行操作的方法。通過調用這些方法,我們能直接得到最大的狀態值及最優化的動作。

public double getMaxQValue(int state)

{

double maxinum = Double.NEGATIVE_INFINITY;

for (int i = 0; i < table[state].length; i++)

{

if (table[state][i] > maxinum)

maxinum = table[state][i];

}

return maxinum;

}

遍歷所有狀態值,從中找出最大化的狀態。

public int getBestAction(int state)

{

double maxinum = Double.NEGATIVE_INFINITY;

int bestAction = 0;

for (int i = 0; i < table[state].length; i++)

{

double qValue = table[state][i];

if (table[state][i] > maxinum) {

maxinum = table[state][i];

bestAction = i;

}

}

return bestAction;

}

遍歷所有狀態值,從中找出最大化的狀態。根據狀態值得到當時最佳動作。

獎賞的確定(Reward)

經過上述狀態和動作離散化,機器人的移動的學習問題已經轉化為一個離散的強化學習問題,現在我們只要選擇Q學習(Q-Learning)算法,直接以Q值作為狀態-動作對的評價值,進行Q(s,a)的強化學習。在開始之前,我們還需要設計一套獎賞規則。通過觀察,在上面動作和狀態發生改變時,特別是機器人本身的狀態發生改變時,機器人的能量都會或增或減。

robocode中一場戰斗開始,每一個機器人都能得到100的能量,當在不同的狀態下,如撞牆,撞到機器人,打中敵人和被敵人打中時,機器人的能量都會發生改變,而且不同的狀態都有不同的能量轉換規則:

1.發射子彈能量大小:我們的機器人在開始時能以不同的能量發射子彈,子彈能量在0.1到3之間。通過getPower()函數我們能得到我們的子彈能量。

2.當機器人撞牆時:能量損傷度=Math.abs(velocity) * 0.5 -1,此處的velocity即撞牆時機器人的速度

3.當機器人被敵人子彈打中時:能量損傷度= 4 * power,如果敵人子彈能量大於1,則能量損傷度+= 2 * (power-1)

4.我們每發射一顆子彈我們的生命能量就會減1

5.有失必有得,如果我們的子彈打中別的機器人,我們可以從子彈那獲得3*power的能量在robocode中我們能通過函數getEnergy()得到自身的能量值。由於能量的這種隨動作和狀態改變的特殊性,我們就以它來定義獎賞,根據上面的規則,實現如下:

1.當機器人撞牆時,獎賞為負能量(Math.abs(getVelocity()) * 0.5 - 1),如下代碼:

public void onHitWall(HitWallEvent e)

{

double change = -(Math.abs(getVelocity()) * 0.5 - 1);

reinforcement += change;

isHitWall = 1;

}

2.當機器人相撞時,獎賞為負能量6,如下代碼:

public void onHitRobot(HitRobotEvent e)

{

double change = -6.0;

reinforcement += change;

}

3.當機器人被子彈打中時,獎賞減少能量(4 * power + 2 * (power - 1))

public void onHitByBullet(HitByBulletEvent e)

{

double power = e.getBullet().getPower();

double change = -(4 * power + 2 * (power - 1));

reinforcement += change;

}

4.當機器人打中敵人時,獎賞增加e.getBullet().getPower() * 3;

public void onBulletHit(BulletHitEvent e)

{

double change = e.getBullet().getPower() * 3;

reinforcement += change;

}

現在我們得到了狀態、動作、Q值、、獎賞等Q學習算法中所有參數,下面我們就以這些參數來實現我們的學習方案。

1.確定環境和行動:

狀態集S = Mapping {s1}

S1中狀態分別為機器人的heading、bearing、distance、hit wall、hit by bullet狀態

2.確定Q值, Q函數Q(s,a)用於映射state/action對到Q值中

3.確定參數:在此根據經驗我們選取如下參數,這些參數值可在實驗中調整(hang原值折現率為0.7,學習率為0.05):

折現率:γ = 0.9;(

)為常量

學習率:a = 0.1;

Q t+1(s t, a t):根據行為"a t"和狀態"S t "得到的新Q

Rt(st, at):根據狀態和行為而得到的獎賞

maxQ(st+1, ai):根據行為"a t"和狀態"S t"得到的最大Q值

3.計算行動選擇概率

4.迭代公式:

Q(s , a)

5.更新Q值

Q t+1(st, at) = (1 - a) Qt(st, at) + [ Rt(st, at) + maxQ(st+1, ai)]

public static final double LearningRate = 0.05;

public static final double DiscountRate = 0.7;

double oldQValue = table.getQValue(lastState, lastAction);

double newQValue = (1 - LearningRate) * oldQValue + LearningRate *

(reinforcement + DiscountRate * table.getMaxQValue(state));

table.setQValue(lastState, lastAction, newQValue);

Agent在經過一定的狀態和執行多個動作后獲得了最終獎賞,這時就會對這個狀態-動作序列分配獎賞。Q學習算法的核心就是每一個狀態和動作的組合都擁有一個Q值,每次獲得最終回報后通過更新等式更新這個Q值。其實這是一個典型的馬爾科夫決策過程(Markov decision process, MDP)。

馬爾科夫決策過程(Markov decision process, MDP):Agent可感知到其環境的不同狀態集合,並且有它可執行的動作集合。在每個離散時間步t,Agent感知到當前狀態st,選擇當前動作at並執行它。環境響應此Agent,給出回報Rt=Q(st, at),並產生一個后繼狀態S t+1= a (s t, a t)。在MDP中,其中函數Q(st, at)稱之為動作評估函數(價值函數),a (s t, a t)稱之為狀態轉換函數;其中Q(st, at)和a (s t, a t)只依賴於當前狀態和動作,而不依賴於以前的狀態和動作。

實現步驟

1.隨機初始化Q值,機器人收集狀態信息,轉換這些狀態信息為離散值

2.根據行動選擇概率公式選擇一個動作執行執行選擇動作操作

3.由行動結果中即機器人的能量改變中得到獎賞

4.從環境中確定新的狀態,迭代修改Q值

5把獎賞和最佳的Q值用於新的狀態中

6.轉2

強化學習同遺傳算法一樣,也要對把機器人同不同對手進行訓練,讓其不斷的自我學習。下面我們以Hang的QLearningBot機器人與不同的例子機器人進行訓練,得到不同的測試結果比較。

為了檢驗訓練的效果,測試實驗設置了多個場景比如:不同的地圖大小,不同的子彈冷卻度等,讓學習機器人完全依靠學習Q表進行自主決策。觀察學習機器人的決策過程,學習結果令我們很滿意:學習機器人能夠躲避敵人的威脅,並找到戰場的最佳位置,找到敵人並及時與有效的打擊。

100個回合與fire機器人的戰斗結果

500個回合與fire機器人的戰斗結果

下面是最初的Q值、選擇動作率、新值、新值與原始值的比較,通過對Q表的數據分析,證明了Q表在逐漸收斂,並最終使Q表收斂到一個穩態,第498回合和499回合先后兩表中各項差的平方和趨近於零。

第一回合

Q-value: -2.6121109205097683

Q-value: -0.9225833518300154

Q-value: -1.9101180820403036

Q-value: -1.5324221662755257

Q-value: -1.0267866181848935

Q-value: -1.3407928359453818

P(a|s): 0.050441674598822324

P(a|s): 0.27323856526964685

P(a|s): 0.10177968729898158

P(a|s): 0.14848834415120124

P(a|s): 0.2461994580991779

P(a|s): 0.1798522705821701

Random Number: 0.46314301126269086

Action selected: 3

Reinforcement: -3.0

Old Q-Value: 0.26091702848069354, New Q-Value: -0.1482071760320772,

Different: -0.40912420451277076

第498回合

Q-value: 0.0

Q-value: 0.0

Q-value: 0.0

Q-value: 0.2457376170002843

Q-value: 0.0

Q-value: 0.0

P(a|s): 0.15927208690906397

P(a|s): 0.15927208690906397

P(a|s): 0.15927208690906397

P(a|s): 0.20363956545468018

P(a|s): 0.15927208690906397

P(a|s): 0.15927208690906397

Random Number: 0.49301116788706556

Action selected: 3

Reinforcement: 0.0

Old Q-Value: 0.0, New Q-Value: 0.022116385530025588,

Different: 0.022116385530025588

第498回合

Q-value: 0.0

Q-value: 0.0

Q-value: 0.0

Q-value: 0.2457376170002843

Q-value: 0.0

Q-value: 0.0

P(a|s): 0.15927208690906397

P(a|s): 0.15927208690906397

P(a|s): 0.15927208690906397

P(a|s): 0.20363956545468018

P(a|s): 0.15927208690906397

P(a|s): 0.15927208690906397

Random Number: 0.5797587942809831

Action selected: 3

Reinforcement: 0.0

Old Q-Value: 0.2457376170002843, New Q-Value: 0.24328024083028146,

Different: -0.0024573761700028285

近來在研究人工智能過程和坦克機器人時,發現國內也開發出了一個類似於Robocode仿真器的平台AI-CODE,其思想延用Robocode,但在Robocode基礎上做了很多的改進,封裝了一些函數模塊,讓開發者更側重於算法和程序設計的學習。最有意思的這個平台能同時支持Java,C,C++,C#語言,從理論上看它支持任何語言。美中不足的是國內應用的例子還不是很多,遠沒有Robocode那么多可參考的例子。如果大家有興趣可嘗試在AI-CODE平台上用不同語言做一些遺傳算法的測試。我想能幫助更多人工智能愛好者。其相關網站大家可上:上去了解。

描述

名字

大小

下載方法

Source code

kevin.zip

16KB

學習

獲得產品和技術

大量測試機器人下載

Robocool,編程游戲愛好者,自由撰稿人,您可以通過聯系到他。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值