回答編集履歴

2

高い塔にするため、岩を縦置きにさせようとした結果を追記

2022/06/19 09:14

投稿

Bongo
Bongo

スコア10807

test CHANGED
@@ -9,165 +9,165 @@
9
9
 
10
10
  public class RockAgent : Agent
11
11
  {
12
- // 後述のrockに付けるタグ
13
- // 別途Tags & Layersでこの名前のタグを登録しておく必要があります
14
- static readonly string RockTag = "Rock";
15
-
16
- // 後述の1エピソードの最大時間
17
- static readonly float EpisodeTime = 10.0f;
18
-
19
- public GameObject rock;
20
- public GameObject rockB;
21
-
22
- // 後述のrockBが置かれている地面オブジェクト
23
- public GameObject ground;
24
-
25
- Rigidbody m_Rb_rock;
26
- Rigidbody m_Rb_rockB;
27
-
28
- // 後述の岩モデルの初期位置
29
- Vector3 initialPosition;
30
- Vector3 initialPositionB;
31
-
32
- // 後述のエピソード開始フラグ
33
- bool episodeBegun;
34
-
35
- // 後述のエピソード中断二乗距離
36
- float distanceThreshold;
37
-
38
- // 後述の残り時間
39
- float timeLeft;
40
-
41
- // rockがrockBを押しのけるようにして地面の上に乗り、地面から落ちることなく
42
- // いつまでもエピソードが終わらないケースがあるようだったので、地面に
43
- // 下記CollisionDetectorをアタッチすることでrockが地面に触れたのを検出し
44
- // エピソードを終了させることにしました
45
- [DisallowMultipleComponent]
46
- [RequireComponent(typeof(Collider))]
47
- private class CollisionDetector : MonoBehaviour
48
- {
49
- public RockAgent Agent { get; set; }
50
-
51
- void OnCollisionEnter(Collision collision)
52
- {
53
- if (collision.gameObject.CompareTag(RockTag))
54
- {
55
- Agent.StopEpisode();
56
- }
57
- }
58
- }
59
-
60
- // CollisionDetectorからもエピソードの停止が行えるよう
61
- // 停止処理を単独のメソッドとして分離しました
62
- void StopEpisode()
63
- {
64
- episodeBegun = false;
65
- SetReward(-1.0f);
66
- EndEpisode();
67
- }
68
-
69
- public override void Initialize()
70
- {
71
- m_Rb_rock = rock.GetComponent<Rigidbody>();
72
- m_Rb_rockB = rockB.GetComponent<Rigidbody>();
73
-
74
- // 前述のCollisionDetectorを地面にアタッチしておきます
75
- var detector = ground.AddComponent<CollisionDetector>();
76
- detector.Agent = this;
77
-
78
- // rockにはタグを付けておきます
79
- rock.tag = RockTag;
80
-
81
- // 今回の実験では、ご質問者さんの岩モデルとは異なる岩モデルを代用品として
82
- // 用意したため、rockの初期位置が(0, 5, 0)、rockBが(0, 0.1, 0)で決め打ちだと
83
- // 私の岩モデルでは少々不都合でした
84
- // そこで、シーン上に配置した時の位置を初期位置とするようにしました
85
- initialPosition = rock.transform.localPosition;
86
- initialPositionB = rockB.transform.localPosition;
87
-
88
- // rockとrockBの初期距離の4倍を限界としました
89
- distanceThreshold = ((initialPosition - initialPositionB) * 4.0f).sqrMagnitude;
90
- }
91
-
92
- public override void OnEpisodeBegin()
93
- {
94
- rock.transform.localPosition = initialPosition;
95
- rock.transform.rotation = Quaternion.Euler(0f, 0f, 0f);
96
- m_Rb_rock.velocity = Vector3.zero;
97
- m_Rb_rock.angularVelocity = Vector3.zero;
98
-
99
- rockB.transform.localPosition = initialPositionB;
100
- rockB.transform.rotation = Quaternion.Euler(0f, 0f, 0f);
101
- m_Rb_rockB.velocity = Vector3.zero;
102
- m_Rb_rockB.angularVelocity = Vector3.zero;
103
-
104
- // 残り時間を初期値に設定
105
- timeLeft = EpisodeTime;
106
-
107
- // エピソード開始時に行動決定およびアクションを要求
108
- RequestDecision();
109
- }
110
-
111
- public override void CollectObservations(VectorSensor sensor)
112
- {
113
- // 念のため申し上げますと、今回の状況設定ではエピソード開始時の
114
- // 岩の姿勢は常に一定ですので、それをエピソード開始時の1回だけ
115
- // 観測したところで変化はなく、行動決定の役には立たなそうな気がします
116
- sensor.AddObservation(rock.transform.localPosition);
117
- sensor.AddObservation(rock.transform.localEulerAngles);
118
- }
119
-
120
- public override void OnActionReceived(ActionBuffers actionBuffers)
121
- {
122
- // エピソード開始時の回転一発で目標姿勢に向けるのであれば、
123
- // 離散アクションより連続アクションの方が適しているんじゃないかと思い
124
- // ContinuousActionsを使うよう変更しました
125
- var rotation = Quaternion.identity;
126
- for (var i = 0; i < 4; i++)
127
- {
128
- rotation[i] = actionBuffers.ContinuousActions[i];
129
- }
130
- rotation.Normalize();
131
- rock.transform.localRotation = rotation;
132
-
133
- // 行動決定はエピソード開始時の1回しか行わないので
134
- // OnActionReceived内で結果を評価するわけにはいかないため
135
- // ここではエピソード開始フラグを立てるだけにしました
136
- episodeBegun = true;
137
- }
138
-
139
- // 結果を評価し報酬を与えるのはFixedUpdate内で行いました
140
- void FixedUpdate()
141
- {
142
- if (!episodeBegun)
143
- {
144
- return;
145
- }
146
-
147
- // rockがおおむね静止している間、残り時間をカウントダウンしていき...
148
- if (m_Rb_rock.velocity.sqrMagnitude < 0.01f)
149
- {
150
- AddReward(1f / EpisodeTime);
151
- timeLeft -= Time.deltaTime;
152
- }
153
-
154
- // EpisodeTime秒持ちこたえたら、ペナルティなしで
155
- // エピソードを終えることにしました
156
- if (timeLeft <= 0.0f)
157
- {
158
- EndEpisode();
159
- }
160
-
161
- // ご質問者さんの条件に加えて、何らかの理由で岩が吹っ飛んで行方不明に
162
- // なった場合に備え、rockとrockBが限界距離を超えて遠ざかった場合にも
163
- // エピソードを中止するようにしました
164
- // また、前述のようにCollisionDetectorが衝突を検出した場合も
165
- // CollisionDetectorによってエピソードが中断されることになります
166
- if (rock.transform.position.y < 0f || (rock.transform.localPosition - rockB.transform.localPosition).sqrMagnitude > distanceThreshold)
167
- {
168
- StopEpisode();
169
- }
170
- }
12
+ // 後述のrockに付けるタグ
13
+ // 別途Tags & Layersでこの名前のタグを登録しておく必要があります
14
+ static readonly string RockTag = "Rock";
15
+
16
+ // 後述の1エピソードの最大時間
17
+ static readonly float EpisodeTime = 10.0f;
18
+
19
+ public GameObject rock;
20
+ public GameObject rockB;
21
+
22
+ // 後述のrockBが置かれている地面オブジェクト
23
+ public GameObject ground;
24
+
25
+ Rigidbody m_Rb_rock;
26
+ Rigidbody m_Rb_rockB;
27
+
28
+ // 後述の岩モデルの初期位置
29
+ Vector3 initialPosition;
30
+ Vector3 initialPositionB;
31
+
32
+ // 後述のエピソード開始フラグ
33
+ bool episodeBegun;
34
+
35
+ // 後述のエピソード中断二乗距離
36
+ float distanceThreshold;
37
+
38
+ // 後述の残り時間
39
+ float timeLeft;
40
+
41
+ // rockがrockBを押しのけるようにして地面の上に乗り、地面から落ちることなく
42
+ // いつまでもエピソードが終わらないケースがあるようだったので、地面に
43
+ // 下記CollisionDetectorをアタッチすることでrockが地面に触れたのを検出し
44
+ // エピソードを終了させることにしました
45
+ [DisallowMultipleComponent]
46
+ [RequireComponent(typeof(Collider))]
47
+ private class CollisionDetector : MonoBehaviour
48
+ {
49
+ public RockAgent Agent { get; set; }
50
+
51
+ void OnCollisionEnter(Collision collision)
52
+ {
53
+ if (collision.gameObject.CompareTag(RockTag))
54
+ {
55
+ Agent.StopEpisode();
56
+ }
57
+ }
58
+ }
59
+
60
+ // CollisionDetectorからもエピソードの停止が行えるよう
61
+ // 停止処理を単独のメソッドとして分離しました
62
+ void StopEpisode()
63
+ {
64
+ episodeBegun = false;
65
+ SetReward(-1.0f);
66
+ EndEpisode();
67
+ }
68
+
69
+ public override void Initialize()
70
+ {
71
+ m_Rb_rock = rock.GetComponent<Rigidbody>();
72
+ m_Rb_rockB = rockB.GetComponent<Rigidbody>();
73
+
74
+ // 前述のCollisionDetectorを地面にアタッチしておきます
75
+ var detector = ground.AddComponent<CollisionDetector>();
76
+ detector.Agent = this;
77
+
78
+ // rockにはタグを付けておきます
79
+ rock.tag = RockTag;
80
+
81
+ // 今回の実験では、ご質問者さんの岩モデルとは異なる岩モデルを代用品として
82
+ // 用意したため、rockの初期位置が(0, 5, 0)、rockBが(0, 0.1, 0)で決め打ちだと
83
+ // 私の岩モデルでは少々不都合でした
84
+ // そこで、シーン上に配置した時の位置を初期位置とするようにしました
85
+ initialPosition = rock.transform.localPosition;
86
+ initialPositionB = rockB.transform.localPosition;
87
+
88
+ // rockとrockBの初期距離の4倍を限界としました
89
+ distanceThreshold = ((initialPosition - initialPositionB) * 4.0f).sqrMagnitude;
90
+ }
91
+
92
+ public override void OnEpisodeBegin()
93
+ {
94
+ rock.transform.localPosition = initialPosition;
95
+ rock.transform.rotation = Quaternion.Euler(0f, 0f, 0f);
96
+ m_Rb_rock.velocity = Vector3.zero;
97
+ m_Rb_rock.angularVelocity = Vector3.zero;
98
+
99
+ rockB.transform.localPosition = initialPositionB;
100
+ rockB.transform.rotation = Quaternion.Euler(0f, 0f, 0f);
101
+ m_Rb_rockB.velocity = Vector3.zero;
102
+ m_Rb_rockB.angularVelocity = Vector3.zero;
103
+
104
+ // 残り時間を初期値に設定
105
+ timeLeft = EpisodeTime;
106
+
107
+ // エピソード開始時に行動決定およびアクションを要求
108
+ RequestDecision();
109
+ }
110
+
111
+ public override void CollectObservations(VectorSensor sensor)
112
+ {
113
+ // 念のため申し上げますと、今回の状況設定ではエピソード開始時の
114
+ // 岩の姿勢は常に一定ですので、それをエピソード開始時の1回だけ
115
+ // 観測したところで変化はなく、行動決定の役には立たなそうな気がします
116
+ sensor.AddObservation(rock.transform.localPosition);
117
+ sensor.AddObservation(rock.transform.localEulerAngles);
118
+ }
119
+
120
+ public override void OnActionReceived(ActionBuffers actionBuffers)
121
+ {
122
+ // エピソード開始時の回転一発で目標姿勢に向けるのであれば、
123
+ // 離散アクションより連続アクションの方が適しているんじゃないかと思い
124
+ // ContinuousActionsを使うよう変更しました
125
+ var rotation = Quaternion.identity;
126
+ for (var i = 0; i < 4; i++)
127
+ {
128
+ rotation[i] = actionBuffers.ContinuousActions[i];
129
+ }
130
+ rotation.Normalize();
131
+ rock.transform.localRotation = rotation;
132
+
133
+ // 行動決定はエピソード開始時の1回しか行わないので
134
+ // OnActionReceived内で結果を評価するわけにはいかないため
135
+ // ここではエピソード開始フラグを立てるだけにしました
136
+ episodeBegun = true;
137
+ }
138
+
139
+ // 結果を評価し報酬を与えるのはFixedUpdate内で行いました
140
+ void FixedUpdate()
141
+ {
142
+ if (!episodeBegun)
143
+ {
144
+ return;
145
+ }
146
+
147
+ // rockがおおむね静止している間、残り時間をカウントダウンしていき...
148
+ if (m_Rb_rock.velocity.sqrMagnitude < 0.01f)
149
+ {
150
+ AddReward(1f / EpisodeTime);
151
+ timeLeft -= Time.deltaTime;
152
+ }
153
+
154
+ // EpisodeTime秒持ちこたえたら、ペナルティなしで
155
+ // エピソードを終えることにしました
156
+ if (timeLeft <= 0.0f)
157
+ {
158
+ EndEpisode();
159
+ }
160
+
161
+ // ご質問者さんの条件に加えて、何らかの理由で岩が吹っ飛んで行方不明に
162
+ // なった場合に備え、rockとrockBが限界距離を超えて遠ざかった場合にも
163
+ // エピソードを中止するようにしました
164
+ // また、前述のようにCollisionDetectorが衝突を検出した場合も
165
+ // CollisionDetectorによってエピソードが中断されることになります
166
+ if (rock.transform.position.y < 0f || (rock.transform.localPosition - rockB.transform.localPosition).sqrMagnitude > distanceThreshold)
167
+ {
168
+ StopEpisode();
169
+ }
170
+ }
171
171
  }
172
172
  ```
173
173
 
@@ -185,3 +185,184 @@
185
185
 
186
186
  念のため申し上げますと、今回の実験では`rock`の初期姿勢しか判断材料に加えていませんので(その初期姿勢にしても、コード中のコメントで申し上げましたように役に立っているとは思えず、実質判断材料なしの手探り)、`rock`や`rockB`の形を変えたり、あるいは`rockB`の位置をずらしただけでも成功率が落ちそうな気がします。
187
187
  いろいろな岩に対応させるには、何らかの手段で岩の形をエージェントに教えてやる必要があるでしょう。たとえば岩の周りからさまざまな角度で[カメラセンサー](https://docs.unity3d.com/Packages/com.unity.ml-agents@2.3/api/Unity.MLAgents.Sensors.CameraSensor.html)で撮影する...とかでしょうかね?
188
+
189
+ ## 高い塔を目指した結果
190
+
191
+ ```C#
192
+ using Unity.MLAgents;
193
+ using Unity.MLAgents.Actuators;
194
+ using Unity.MLAgents.Sensors;
195
+ using UnityEngine;
196
+
197
+ public class RockAgent : Agent
198
+ {
199
+ static readonly string RockTag = "Rock";
200
+ static readonly float EpisodeTime = 10.0f;
201
+
202
+ public GameObject rock;
203
+ public GameObject rockB;
204
+ public GameObject ground;
205
+
206
+ float distanceThreshold;
207
+ bool episodeBegun;
208
+ Vector3 initialPosition;
209
+ Vector3 initialPositionB;
210
+ Rigidbody m_Rb_rock;
211
+ Rigidbody m_Rb_rockB;
212
+ float timeLeft;
213
+
214
+ void StopEpisode()
215
+ {
216
+ // 失敗時のペナルティを増やしました
217
+ episodeBegun = false;
218
+ SetReward(-4.0f);
219
+ EndEpisode();
220
+ }
221
+
222
+ [DisallowMultipleComponent]
223
+ [RequireComponent(typeof(Collider))]
224
+ private class CollisionDetector : MonoBehaviour
225
+ {
226
+ public RockAgent Agent { get; set; }
227
+
228
+ private void OnCollisionEnter(Collision collision)
229
+ {
230
+ if (collision.gameObject.CompareTag(RockTag))
231
+ {
232
+ Agent.StopEpisode();
233
+ }
234
+ }
235
+ }
236
+
237
+ public override void Initialize()
238
+ {
239
+ m_Rb_rock = rock.GetComponent<Rigidbody>();
240
+ m_Rb_rockB = rockB.GetComponent<Rigidbody>();
241
+ var detector = ground.AddComponent<CollisionDetector>();
242
+ detector.Agent = this;
243
+ rock.tag = RockTag;
244
+ initialPosition = rock.transform.localPosition;
245
+ initialPositionB = rockB.transform.localPosition;
246
+ distanceThreshold = ((initialPosition - initialPositionB) * 4.0f).sqrMagnitude;
247
+ }
248
+
249
+ public override void OnEpisodeBegin()
250
+ {
251
+ rock.transform.localPosition = initialPosition;
252
+ rock.transform.rotation = Quaternion.Euler(0f, 0f, 0f);
253
+ m_Rb_rock.velocity = Vector3.zero;
254
+ m_Rb_rock.angularVelocity = Vector3.zero;
255
+ rockB.transform.localPosition = initialPositionB;
256
+ rockB.transform.rotation = Quaternion.Euler(0f, 0f, 0f);
257
+ m_Rb_rockB.velocity = Vector3.zero;
258
+ m_Rb_rockB.angularVelocity = Vector3.zero;
259
+ timeLeft = EpisodeTime;
260
+ RequestDecision();
261
+ }
262
+
263
+ public override void CollectObservations(VectorSensor sensor)
264
+ {
265
+ sensor.AddObservation(rock.transform.localPosition);
266
+ sensor.AddObservation(rock.transform.localEulerAngles);
267
+ }
268
+
269
+ public override void OnActionReceived(ActionBuffers actionBuffers)
270
+ {
271
+ // 慣性モーメントが最も小さい方角が長軸だと仮定し、その軸を縦に置いた姿勢から
272
+ // 一定角度(さしあたり15°)以内の範囲で回転量を決めさせることにしました
273
+ var actions = actionBuffers.ContinuousActions;
274
+ var inertiaTensor = m_Rb_rock.inertiaTensor;
275
+ var axis = Vector3.zero;
276
+ if (inertiaTensor.x < inertiaTensor.y)
277
+ {
278
+ if (inertiaTensor.x < inertiaTensor.z)
279
+ {
280
+ axis.x = 1.0f;
281
+ }
282
+ else
283
+ {
284
+ axis.z = 1.0f;
285
+ }
286
+ }
287
+ else
288
+ {
289
+ if (inertiaTensor.y < inertiaTensor.z)
290
+ {
291
+ axis.y = 1.0f;
292
+ }
293
+ else
294
+ {
295
+ axis.z = 1.0f;
296
+ }
297
+ }
298
+ var rotation = Quaternion.FromToRotation(
299
+ m_Rb_rock.inertiaTensorRotation * axis,
300
+ actions[3] > 0.0f ? Vector3.up : Vector3.down);
301
+ rotation = Quaternion.AngleAxis(actions[3], Vector3.up) * rotation;
302
+ var targetDirection = new Vector3(actions[0], actions[1], actions[2]).normalized;
303
+ if (targetDirection.sqrMagnitude > 0.0f)
304
+ {
305
+ rotation = Quaternion.RotateTowards(
306
+ Quaternion.identity,
307
+ Quaternion.FromToRotation(Vector3.up, targetDirection),
308
+ 15.0f) * rotation;
309
+ }
310
+ rock.transform.localRotation = rotation;
311
+
312
+ // 回転だけできれいに乗る位置を決めさせるのは酷なように思い、
313
+ // 位置を少しずらすことも許すようにしました
314
+ // ただし、ずれに応じていくらか減点しています
315
+ var shift = Vector3.ClampMagnitude(
316
+ new Vector3(actions[4], 0.0f, actions[5]),
317
+ 0.0625f);
318
+ rock.transform.localPosition += shift;
319
+ AddReward(-shift.sqrMagnitude * 16.0f);
320
+
321
+ episodeBegun = true;
322
+ }
323
+
324
+ void FixedUpdate()
325
+ {
326
+ if (!episodeBegun)
327
+ {
328
+ return;
329
+ }
330
+
331
+ if (m_Rb_rock.velocity.sqrMagnitude < 0.01f)
332
+ {
333
+ // 静止時の獲得報酬が多すぎたように感じ、引き下げました
334
+ AddReward((4.0f * Time.deltaTime) / EpisodeTime);
335
+ timeLeft -= Time.deltaTime;
336
+ }
337
+
338
+ if (timeLeft <= 0.0f)
339
+ {
340
+ // 適当な高さ(さしあたりinitialPosition.yの2倍)から適当なサイズのBoxCastを行い...
341
+ var origin = ground.transform.position;
342
+ if (Physics.BoxCast(
343
+ new Vector3(origin.x, initialPosition.y * 2.0f, origin.z),
344
+ new Vector3(1.0f, 1.0f, 0.01f),
345
+ Vector3.down,
346
+ out var hitInfo))
347
+ {
348
+ // 塔の高さを測定して、高さに応じたボーナス点を与えることにしました
349
+ // このボーナス点の式は勘で決めたもので、根拠があるわけではありません
350
+ AddReward(Mathf.Pow(Mathf.Clamp01((hitInfo.point.y - origin.y) / initialPosition.y), 8.0f) * 32.0f);
351
+ }
352
+
353
+ EndEpisode();
354
+ }
355
+
356
+ if ((rock.transform.position.y < 0f) ||
357
+ ((rock.transform.localPosition - rockB.transform.localPosition).sqrMagnitude >
358
+ distanceThreshold))
359
+ {
360
+ StopEpisode();
361
+ }
362
+ }
363
+ }
364
+ ```
365
+
366
+ ![図4](https://ddjkaamml8q8x.cloudfront.net/questions/2022-06-19/68fca761-5851-4b2c-b3be-b82a38976ded.png)
367
+
368
+ ![図5](https://ddjkaamml8q8x.cloudfront.net/questions/2022-06-19/44caf904-2ed2-4422-950a-d5d4ce39cfb9.gif)

1

コンポーネント名のつづり間違いを修正

2022/06/05 21:49

投稿

Bongo
Bongo

スコア10807

test CHANGED
@@ -1,4 +1,4 @@
1
- エージェントに`DescisionRequester`はアタッチされているでしょうか?あれがあると設定した周期で自動的に行動決定要求を出してくるので、今回の目的には邪魔になるかと思います。もし`DescisionRequester`があれば、ひとまずそれは削除してしまうのがいいでしょう。
1
+ エージェントに`DecisionRequester`はアタッチされているでしょうか?あれがあると設定した周期で自動的に行動決定要求を出してくるので、今回の目的には邪魔になるかと思います。もし`DecisionRequester`があれば、ひとまずそれは削除してしまうのがいいでしょう。
2
2
  その上で、下記のように`OnEpisodeBegin`で一発だけ`RequestDecision`を実行してはいかがでしょうか。
3
3
 
4
4
  ```C#