## ข้อกำหนดเบื้องต้น ในบทเรียนนี้ เราจะใช้ไลบรารีที่เรียกว่า **OpenAI Gym** เพื่อจำลอง **สภาพแวดล้อม** ต่างๆ คุณสามารถรันโค้ดของบทเรียนนี้ในเครื่องของคุณเอง (เช่น จาก Visual Studio Code) ซึ่งการจำลองจะเปิดในหน้าต่างใหม่ หากคุณรันโค้ดออนไลน์ คุณอาจต้องปรับแต่งโค้ดเล็กน้อยตามที่อธิบายไว้ [ที่นี่](https://towardsdatascience.com/rendering-openai-gym-envs-on-binder-and-google-colab-536f99391cc7) ## OpenAI Gym ในบทเรียนก่อน กฎของเกมและสถานะถูกกำหนดโดยคลาส `Board` ซึ่งเราได้สร้างขึ้นเอง ในที่นี้เราจะใช้ **สภาพแวดล้อมจำลอง** พิเศษ ซึ่งจะจำลองฟิสิกส์ของการทรงตัวของเสา หนึ่งในสภาพแวดล้อมจำลองที่ได้รับความนิยมมากที่สุดสำหรับการฝึกอัลกอริทึมการเรียนรู้แบบเสริมกำลังคือ [Gym](https://gym.openai.com/) ซึ่งได้รับการดูแลโดย [OpenAI](https://openai.com/) โดยการใช้ Gym นี้ เราสามารถสร้าง **สภาพแวดล้อม** ต่างๆ ตั้งแต่การจำลอง CartPole ไปจนถึงเกม Atari > **หมายเหตุ**: คุณสามารถดูสภาพแวดล้อมอื่นๆ ที่มีใน OpenAI Gym [ที่นี่](https://gym.openai.com/envs/#classic_control) ก่อนอื่น มาติดตั้ง Gym และนำเข้าไลบรารีที่จำเป็น (code block 1): ```python import sys !{sys.executable} -m pip install gym import gym import matplotlib.pyplot as plt import numpy as np import random ``` ## แบบฝึกหัด - การเริ่มต้นสภาพแวดล้อม CartPole ในการทำงานกับปัญหาการทรงตัวของ CartPole เราจำเป็นต้องเริ่มต้นสภาพแวดล้อมที่เกี่ยวข้อง สภาพแวดล้อมแต่ละแห่งจะมี: - **Observation space** ที่กำหนดโครงสร้างของข้อมูลที่เราได้รับจากสภาพแวดล้อม สำหรับปัญหา CartPole เราจะได้รับตำแหน่งของเสา ความเร็ว และค่าบางอย่างอื่นๆ - **Action space** ที่กำหนดการกระทำที่เป็นไปได้ ในกรณีของเรา Action space เป็นแบบไม่ต่อเนื่อง และประกอบด้วยสองการกระทำ - **ซ้าย** และ **ขวา** (code block 2) 1. ในการเริ่มต้น ให้พิมพ์โค้ดต่อไปนี้: ```python env = gym.make("CartPole-v1") print(env.action_space) print(env.observation_space) print(env.action_space.sample()) ``` เพื่อดูว่าสภาพแวดล้อมทำงานอย่างไร ลองรันการจำลองสั้นๆ เป็นเวลา 100 ขั้นตอน ในแต่ละขั้นตอน เราจะให้การกระทำหนึ่งอย่างที่ต้องทำ - ในการจำลองนี้ เราเพียงแค่สุ่มเลือกการกระทำจาก `action_space` 1. รันโค้ดด้านล่างและดูผลลัพธ์ที่ได้ ✅ จำไว้ว่าควรรันโค้ดนี้ในเครื่อง Python ที่ติดตั้งในเครื่องของคุณ! (code block 3) ```python env.reset() for i in range(100): env.render() env.step(env.action_space.sample()) env.close() ``` คุณควรเห็นภาพที่คล้ายกับภาพนี้: ![non-balancing cartpole](../../../../8-Reinforcement/2-Gym/images/cartpole-nobalance.gif) 1. ระหว่างการจำลอง เราจำเป็นต้องได้รับข้อมูลการสังเกตเพื่อที่จะตัดสินใจว่าจะทำอะไรต่อไป ในความเป็นจริง ฟังก์ชัน step จะคืนค่าการสังเกตปัจจุบัน ฟังก์ชันรางวัล และธง done ที่ระบุว่าควรดำเนินการจำลองต่อไปหรือไม่: (code block 4) ```python env.reset() done = False while not done: env.render() obs, rew, done, info = env.step(env.action_space.sample()) print(f"{obs} -> {rew}") env.close() ``` คุณจะเห็นผลลัพธ์ที่คล้ายกับนี้ในผลลัพธ์ของโน้ตบุ๊ก: ```text [ 0.03403272 -0.24301182 0.02669811 0.2895829 ] -> 1.0 [ 0.02917248 -0.04828055 0.03248977 0.00543839] -> 1.0 [ 0.02820687 0.14636075 0.03259854 -0.27681916] -> 1.0 [ 0.03113408 0.34100283 0.02706215 -0.55904489] -> 1.0 [ 0.03795414 0.53573468 0.01588125 -0.84308041] -> 1.0 ... [ 0.17299878 0.15868546 -0.20754175 -0.55975453] -> 1.0 [ 0.17617249 0.35602306 -0.21873684 -0.90998894] -> 1.0 ``` เวกเตอร์การสังเกตที่คืนค่ามาในแต่ละขั้นตอนของการจำลองประกอบด้วยค่าต่อไปนี้: - ตำแหน่งของรถเข็น - ความเร็วของรถเข็น - มุมของเสา - อัตราการหมุนของเสา 1. รับค่าต่ำสุดและค่าสูงสุดของตัวเลขเหล่านั้น: (code block 5) ```python print(env.observation_space.low) print(env.observation_space.high) ``` คุณอาจสังเกตเห็นว่าค่ารางวัลในแต่ละขั้นตอนของการจำลองมีค่าเท่ากับ 1 เสมอ นั่นเป็นเพราะเป้าหมายของเราคือการอยู่รอดให้นานที่สุดเท่าที่จะเป็นไปได้ กล่าวคือ รักษาเสาให้อยู่ในตำแหน่งแนวตั้งในระดับที่เหมาะสมให้นานที่สุด ✅ ในความเป็นจริง การจำลอง CartPole ถือว่าแก้ปัญหาได้หากเราสามารถรับรางวัลเฉลี่ย 195 ในการทดลองต่อเนื่อง 100 ครั้ง ## การทำสถานะให้เป็นแบบไม่ต่อเนื่อง ใน Q-Learning เราจำเป็นต้องสร้าง Q-Table ที่กำหนดว่าจะทำอะไรในแต่ละสถานะ เพื่อที่จะทำสิ่งนี้ได้ เราจำเป็นต้องให้สถานะเป็นแบบ **ไม่ต่อเนื่อง** กล่าวคือ ควรมีจำนวนค่าที่ไม่ต่อเนื่องที่จำกัด ดังนั้นเราจำเป็นต้อง **ทำสถานะให้เป็นแบบไม่ต่อเนื่อง** โดยการแมปค่าการสังเกตไปยังชุดสถานะที่จำกัด มีวิธีการบางอย่างที่เราสามารถทำได้: - **แบ่งเป็นช่วง** หากเรารู้ช่วงของค่าบางค่า เราสามารถแบ่งช่วงนี้ออกเป็นจำนวน **ช่วง** และแทนค่าด้วยหมายเลขช่วงที่มันอยู่ วิธีนี้สามารถทำได้โดยใช้เมธอด [`digitize`](https://numpy.org/doc/stable/reference/generated/numpy.digitize.html) ของ numpy ในกรณีนี้ เราจะทราบขนาดของสถานะอย่างแม่นยำ เพราะมันจะขึ้นอยู่กับจำนวนช่วงที่เราเลือกสำหรับการทำให้เป็นแบบดิจิทัล ✅ เราสามารถใช้การแทรกเชิงเส้นเพื่อปรับค่ามาอยู่ในช่วงที่จำกัด (เช่น จาก -20 ถึง 20) และจากนั้นแปลงตัวเลขเป็นจำนวนเต็มโดยการปัดเศษ วิธีนี้จะให้การควบคุมขนาดของสถานะน้อยลง โดยเฉพาะอย่างยิ่งหากเราไม่ทราบช่วงที่แน่นอนของค่าขาเข้า ตัวอย่างเช่น ในกรณีของเรา 2 ใน 4 ค่าจะไม่มีขอบเขตบน/ล่าง ซึ่งอาจส่งผลให้มีจำนวนสถานะที่ไม่มีที่สิ้นสุด ในตัวอย่างของเรา เราจะใช้วิธีที่สอง ตามที่คุณอาจสังเกตเห็นในภายหลัง แม้จะไม่มีขอบเขตบน/ล่าง ค่าดังกล่าวมักจะไม่ค่อยมีค่าที่อยู่นอกช่วงที่จำกัด ดังนั้นสถานะที่มีค่าที่สุดขั้วจะเกิดขึ้นได้ยาก 1. นี่คือฟังก์ชันที่จะรับค่าการสังเกตจากโมเดลของเราและสร้างทูเพิลของค่าจำนวนเต็ม 4 ค่า: (code block 6) ```python def discretize(x): return tuple((x/np.array([0.25, 0.25, 0.01, 0.1])).astype(np.int)) ``` 1. ลองสำรวจวิธีการทำให้เป็นแบบไม่ต่อเนื่องด้วยช่วงอีกวิธีหนึ่ง: (code block 7) ```python def create_bins(i,num): return np.arange(num+1)*(i[1]-i[0])/num+i[0] print("Sample bins for interval (-5,5) with 10 bins\n",create_bins((-5,5),10)) ints = [(-5,5),(-2,2),(-0.5,0.5),(-2,2)] # intervals of values for each parameter nbins = [20,20,10,10] # number of bins for each parameter bins = [create_bins(ints[i],nbins[i]) for i in range(4)] def discretize_bins(x): return tuple(np.digitize(x[i],bins[i]) for i in range(4)) ``` 1. ตอนนี้ลองรันการจำลองสั้นๆ และสังเกตค่าของสภาพแวดล้อมที่ไม่ต่อเนื่อง ลองใช้ทั้ง `discretize` และ `discretize_bins` และดูว่ามีความแตกต่างหรือไม่ ✅ `discretize_bins` คืนหมายเลขช่วง ซึ่งเริ่มต้นที่ 0 ดังนั้นสำหรับค่าของตัวแปรขาเข้าที่อยู่รอบๆ 0 จะคืนค่าจากกลางช่วง (10) ใน `discretize` เราไม่ได้สนใจช่วงของค่าผลลัพธ์ ทำให้ค่าของสถานะไม่ถูกเลื่อน และ 0 ตรงกับ 0 (code block 8) ```python env.reset() done = False while not done: #env.render() obs, rew, done, info = env.step(env.action_space.sample()) #print(discretize_bins(obs)) print(discretize(obs)) env.close() ``` ✅ ยกเลิกการคอมเมนต์บรรทัดที่เริ่มต้นด้วย env.render หากคุณต้องการดูว่าสภาพแวดล้อมทำงานอย่างไร มิฉะนั้นคุณสามารถรันมันในพื้นหลัง ซึ่งจะเร็วกว่า เราจะใช้การรันแบบ "มองไม่เห็น" นี้ในระหว่างกระบวนการ Q-Learning ของเรา ## โครงสร้างของ Q-Table ในบทเรียนก่อน สถานะเป็นคู่ของตัวเลขง่ายๆ จาก 0 ถึง 8 และดังนั้นจึงสะดวกที่จะแสดง Q-Table ด้วยเทนเซอร์ numpy ที่มีรูปร่าง 8x8x2 หากเราใช้การทำให้เป็นแบบไม่ต่อเนื่องด้วยช่วง ขนาดของเวกเตอร์สถานะของเราก็จะทราบเช่นกัน ดังนั้นเราสามารถใช้วิธีเดียวกันและแสดงสถานะด้วยอาร์เรย์ที่มีรูปร่าง 20x20x10x10x2 (ที่นี่ 2 คือมิติของ Action space และมิติแรกสอดคล้องกับจำนวนช่วงที่เราเลือกใช้สำหรับแต่ละพารามิเตอร์ใน Observation space) อย่างไรก็ตาม บางครั้งขนาดที่แน่นอนของ Observation space อาจไม่ทราบ ในกรณีของฟังก์ชัน `discretize` เราอาจไม่สามารถมั่นใจได้ว่าสถานะของเราจะอยู่ในขอบเขตที่แน่นอน เพราะค่าบางค่าของต้นฉบับไม่มีขอบเขต ดังนั้นเราจะใช้วิธีที่แตกต่างออกไปเล็กน้อยและแสดง Q-Table ด้วยดิกชันนารี 1. ใช้คู่ *(state,action)* เป็นคีย์ของดิกชันนารี และค่าจะสอดคล้องกับค่าของ Q-Table (code block 9) ```python Q = {} actions = (0,1) def qvalues(state): return [Q.get((state,a),0) for a in actions] ``` ที่นี่เรายังได้กำหนดฟังก์ชัน `qvalues()` ซึ่งคืนค่ารายการ Q-Table สำหรับสถานะที่กำหนดซึ่งสอดคล้องกับการกระทำที่เป็นไปได้ทั้งหมด หากไม่มีรายการใน Q-Table เราจะคืนค่า 0 เป็นค่าเริ่มต้น ## มาเริ่ม Q-Learning กันเถอะ ตอนนี้เราพร้อมที่จะสอน Peter ให้ทรงตัวแล้ว! 1. ก่อนอื่น มาตั้งค่าพารามิเตอร์ไฮเปอร์บางตัว: (code block 10) ```python # hyperparameters alpha = 0.3 gamma = 0.9 epsilon = 0.90 ``` ที่นี่ `alpha` คือ **learning rate** ที่กำหนดว่าเราควรปรับค่าปัจจุบันของ Q-Table ในแต่ละขั้นตอนมากน้อยเพียงใด ในบทเรียนก่อนเราเริ่มต้นด้วย 1 และจากนั้นลด `alpha` ลงในระหว่างการฝึก ในตัวอย่างนี้เราจะคงค่าคงที่ไว้เพื่อความเรียบง่าย และคุณสามารถทดลองปรับค่าของ `alpha` ได้ในภายหลัง `gamma` คือ **discount factor** ที่แสดงว่าเราควรให้ความสำคัญกับรางวัลในอนาคตมากกว่ารางวัลปัจจุบันมากน้อยเพียงใด `epsilon` คือ **exploration/exploitation factor** ที่กำหนดว่าเราควรเลือกการสำรวจมากกว่าการใช้ประโยชน์หรือไม่ ในอัลกอริทึมของเรา เราจะเลือกการกระทำถัดไปตามค่าของ Q-Table ในเปอร์เซ็นต์ของกรณีที่กำหนดโดย `epsilon` และในจำนวนที่เหลือเราจะดำเนินการแบบสุ่ม สิ่งนี้จะช่วยให้เราสำรวจพื้นที่การค้นหาที่เราไม่เคยเห็นมาก่อน ✅ ในแง่ของการทรงตัว - การเลือกการกระทำแบบสุ่ม (การสำรวจ) จะทำหน้าที่เป็นการผลักแบบสุ่มในทิศทางที่ผิด และเสาจะต้องเรียนรู้วิธีการฟื้นฟูสมดุลจาก "ข้อผิดพลาด" เหล่านั้น ### ปรับปรุงอัลกอริทึม เรายังสามารถปรับปรุงอัลกอริทึมของเราจากบทเรียนก่อน: - **คำนวณรางวัลสะสมเฉลี่ย** ในการจำลองจำนวนหนึ่ง เราจะพิมพ์ความคืบหน้าทุกๆ 5000 รอบ และเราจะเฉลี่ยรางวัลสะสมในช่วงเวลานั้น หมายความว่าหากเราได้คะแนนมากกว่า 195 เราสามารถถือว่าปัญหาได้รับการแก้ไขแล้ว ด้วยคุณภาพที่สูงกว่าที่กำหนด - **คำนวณผลลัพธ์สะสมเฉลี่ยสูงสุด** `Qmax` และเราจะเก็บ Q-Table ที่สอดคล้องกับผลลัพธ์นั้น เมื่อคุณรันการฝึก คุณจะสังเกตเห็นว่าบางครั้งผลลัพธ์สะสมเฉลี่ยเริ่มลดลง และเราต้องการเก็บค่าของ Q-Table ที่สอดคล้องกับโมเดลที่ดีที่สุดที่สังเกตได้ระหว่างการฝึก 1. เก็บรางวัลสะสมทั้งหมดในแต่ละการจำลองไว้ในเวกเตอร์ `rewards` เพื่อการพล็อตในภายหลัง (code block 11) ```python def probs(v,eps=1e-4): v = v-v.min()+eps v = v/v.sum() return v Qmax = 0 cum_rewards = [] rewards = [] for epoch in range(100000): obs = env.reset() done = False cum_reward=0 # == do the simulation == while not done: s = discretize(obs) if random.random() Qmax: Qmax = np.average(cum_rewards) Qbest = Q cum_rewards=[] ``` สิ่งที่คุณอาจสังเกตเห็นจากผลลัพธ์เหล่านั้น: - **ใกล้เป้าหมายของเรา** เราใกล้จะบรรลุเป้าหมายในการได้รับรางวัลสะสม 195 ในการรันการจำลองต่อเนื่อง 100+ ครั้ง หรือเราอาจบรรลุเป้าหมายแล้ว! แม้ว่าเราจะได้ตัวเลขที่น้อยกว่า เราก็ยังไม่ทราบ เพราะเราเฉลี่ยมากกว่า 5000 รอบ และมีเพียง 100 รอบที่จำเป็นในเกณฑ์อย่างเป็นทางการ - **รางวัลเริ่มลดลง** บางครั้งรางวัลเริ่มลดลง ซึ่งหมายความว่าเราอาจ "ทำลาย" ค่าที่เรียนรู้แล้วใน Q-Table ด้วยค่าที่ทำให้สถานการณ์แย่ลง การสังเกตนี้จะเห็นได้ชัดเจนขึ้นหากเราพล็อตความคืบหน้าการฝึก ## การพล็อตความคืบหน้าการฝึก ระหว่างการฝึก เราได้เก็บค่ารางวัลสะสมในแต่ละรอบไว้ในเวกเตอร์ `rewards` นี่คือสิ่งที่มันดูเหมือนเมื่อเราพล็อตมันกับหมายเลขรอบ: ```python plt.plot(rewards) ``` ![raw progress](../../../../8-Reinforcement/2-Gym/images/train_progress_raw.png) จากกราฟนี้ ไม่สามารถบอกอะไรได้ เพราะลักษณะของกระบวนการฝึกแบบสุ่มทำให้ความยาวของเซสชันการฝึกแตกต่างกันมาก เพื่อให้กราฟนี้มีความหมายมากขึ้น เราสามารถคำนวณ **ค่าเฉลี่ยเคลื่อนที่** ในการทดลองชุดหนึ่ง เช่น 100 สิ่งนี้สามารถทำได้อย่างสะดวกโดยใช้ `np.convolve`: (code block 12) ```python def running_average(x,window): return np.convolve(x,np.ones(window)/window,mode='valid') plt.plot(running_average(rewards,100)) ``` ![training progress](../../../../8-Reinforcement/2-Gym/images/train_progress_runav.png) ## การปรับพารามิเตอร์ไฮเปอร์ เพื่อให้การเรียนรู้มีเสถียรภาพมากขึ้น มีเหตุผลที่จะปรับพารามิเตอร์ไฮเปอร์บางตัวระหว่างการฝึก โดยเฉพาะ: - **สำหรับ learning rate** `alpha` เราอาจเริ่มต้นด้วยค่าที่ใกล้เคียงกับ 1 และจากนั้นลดค่าพารามิเตอร์ลงเรื่อยๆ เมื่อเวลาผ่านไป เราจะได้รับค่าความน่าจะเป็นที่ดีใน Q-Table และดังนั้นเราควรปรับค่าพวกนั้นเล็กน้อย และไม่เขียนทับด้วยค่าที่ใหม่ทั้งหมด - **เพิ่ม epsilon** เราอาจต้องการเพิ่ม `epsilon` อย่างช้าๆ เพื่อสำรวจน้อยลงและใช้ประโยชน์มากขึ้น อาจมีเหตุผลที่จะเริ่มต้นด้วยค่าที่ต่ำของ `epsilon` และเพิ่มขึ้นจนเกือบถึง 1 > **งานที่ 1**: ลองปรับค่าพารามิเตอร์ต่าง ๆ และดูว่าคุณสามารถทำให้ผลตอบแทนรวมสูงขึ้นได้หรือไม่ คุณได้คะแนนเกิน 195 หรือยัง? > **งานที่ 2**: เพื่อแก้ปัญหาอย่างเป็นทางการ คุณจำเป็นต้องได้ค่าเฉลี่ยรางวัล 195 ในการทดลอง 100 ครั้งติดต่อกัน วัดผลระหว่างการฝึกและตรวจสอบให้แน่ใจว่าคุณได้แก้ปัญหาอย่างเป็นทางการแล้ว! ## ดูผลลัพธ์ในทางปฏิบัติ มันน่าสนใจที่จะเห็นว่ารูปแบบที่ฝึกมาแล้วทำงานอย่างไร ลองรันการจำลองและใช้กลยุทธ์การเลือกการกระทำแบบเดียวกับที่ใช้ระหว่างการฝึก โดยสุ่มตามการแจกแจงความน่าจะเป็นใน Q-Table: (code block 13) ```python obs = env.reset() done = False while not done: s = discretize(obs) env.render() v = probs(np.array(qvalues(s))) a = random.choices(actions,weights=v)[0] obs,_,done,_ = env.step(a) env.close() ``` คุณควรเห็นบางสิ่งที่คล้ายกับนี้: ![a balancing cartpole](../../../../8-Reinforcement/2-Gym/images/cartpole-balance.gif) --- ## 🚀ความท้าทาย > **งานที่ 3**: ในที่นี้ เราใช้สำเนาสุดท้ายของ Q-Table ซึ่งอาจไม่ใช่ตัวที่ดีที่สุด อย่าลืมว่าเราได้บันทึก Q-Table ที่มีประสิทธิภาพดีที่สุดไว้ในตัวแปร `Qbest`! ลองใช้ตัวอย่างเดียวกันกับ Q-Table ที่มีประสิทธิภาพดีที่สุดโดยคัดลอก `Qbest` ไปยัง `Q` และดูว่าคุณสังเกตเห็นความแตกต่างหรือไม่ > **งานที่ 4**: ในที่นี้ เราไม่ได้เลือกการกระทำที่ดีที่สุดในแต่ละขั้นตอน แต่สุ่มตามการแจกแจงความน่าจะเป็นที่เกี่ยวข้อง จะสมเหตุสมผลกว่าหรือไม่ถ้าเลือกการกระทำที่ดีที่สุดเสมอ ซึ่งมีค่าที่สูงที่สุดใน Q-Table? สิ่งนี้สามารถทำได้โดยใช้ฟังก์ชัน `np.argmax` เพื่อค้นหาหมายเลขการกระทำที่สอดคล้องกับค่าที่สูงที่สุดใน Q-Table ลองใช้กลยุทธ์นี้และดูว่ามันช่วยปรับปรุงการทรงตัวหรือไม่ ## [แบบทดสอบหลังการบรรยาย](https://ff-quizzes.netlify.app/en/ml/) ## งานที่ได้รับมอบหมาย [ฝึก Mountain Car](assignment.md) ## สรุป ตอนนี้เราได้เรียนรู้วิธีการฝึกตัวแทนเพื่อให้ได้ผลลัพธ์ที่ดีเพียงแค่ให้ฟังก์ชันรางวัลที่กำหนดสถานะที่ต้องการของเกม และให้โอกาสพวกเขาสำรวจพื้นที่ค้นหาอย่างชาญฉลาด เราได้ใช้ Q-Learning algorithm สำเร็จในกรณีของสภาพแวดล้อมแบบไม่ต่อเนื่องและต่อเนื่อง แต่มีการกระทำแบบไม่ต่อเนื่อง สิ่งสำคัญคือต้องศึกษาสถานการณ์ที่สถานะการกระทำเป็นแบบต่อเนื่อง และเมื่อพื้นที่การสังเกตมีความซับซ้อนมากขึ้น เช่น ภาพจากหน้าจอเกม Atari ในปัญหาเหล่านี้เรามักต้องใช้เทคนิคการเรียนรู้ของเครื่องที่ทรงพลังมากขึ้น เช่น neural networks เพื่อให้ได้ผลลัพธ์ที่ดี หัวข้อที่ก้าวหน้ากว่านี้จะเป็นเนื้อหาในหลักสูตร AI ขั้นสูงของเราในอนาคต --- **ข้อจำกัดความรับผิดชอบ**: เอกสารนี้ได้รับการแปลโดยใช้บริการแปลภาษา AI [Co-op Translator](https://github.com/Azure/co-op-translator) แม้ว่าเราจะพยายามให้การแปลมีความถูกต้องมากที่สุด แต่โปรดทราบว่าการแปลอัตโนมัติอาจมีข้อผิดพลาดหรือความไม่ถูกต้อง เอกสารต้นฉบับในภาษาดั้งเดิมควรถือเป็นแหล่งข้อมูลที่เชื่อถือได้ สำหรับข้อมูลที่สำคัญ ขอแนะนำให้ใช้บริการแปลภาษามืออาชีพ เราไม่รับผิดชอบต่อความเข้าใจผิดหรือการตีความผิดที่เกิดจากการใช้การแปลนี้