diff --git a/.github/workflows/ros-ci.yml b/.github/workflows/ros-ci.yml index 903cf76..5e8d842 100644 --- a/.github/workflows/ros-ci.yml +++ b/.github/workflows/ros-ci.yml @@ -49,6 +49,23 @@ jobs: run: | printf "[install]\nbreak-system-packages = true\n" | sudo tee /etc/pip.conf + - name: Check and Install ROS dependencies + shell: bash + run: | + set -e + source /opt/ros/${{ matrix.ros_distribution }}/setup.bash + echo "--- Updating rosdep definitions ---" + rosdep update + echo "--- Installing system dependencies for ROS 2 ${{ matrix.ros_distribution }} ---" + rosdep install --from-paths ros_ws/src --ignore-src -y -r --rosdistro ${{ matrix.ros_distribution }} + echo "--- Performing rosdep check for ROS 2 ${{ matrix.ros_distribution }} ---" + if rosdep check --from-paths ros_ws/src --ignore-src --rosdistro ${{ matrix.ros_distribution }}; then + echo "--- rosdep check passed ---" + else + echo "--- rosdep check failed: Missing system dependencies or unresolvable keys. ---" + exit 1 + fi + - name: Build and Test uses: ros-tooling/action-ros-ci@v0.3 env: diff --git a/README.md b/README.md index 011fb0f..dbdc612 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # TurtleBot3 -- Active Branches: noetic, humble, jazzy, main(rolling) -- Legacy Branches: *-devel +- Active Branches: humble, jazzy, main(rolling) +- Legacy Branches: *-devel, noetic ## Open Source Projects Related to TurtleBot3 - [turtlebot3](https://github.com/ROBOTIS-GIT/turtlebot3) @@ -17,6 +17,7 @@ - [turtlebot3_home_service_challenge](https://github.com/ROBOTIS-GIT/turtlebot3_home_service_challenge) - [hls_lfcd_lds_driver](https://github.com/ROBOTIS-GIT/hls_lfcd_lds_driver) - [ld08_driver](https://github.com/ROBOTIS-GIT/ld08_driver) +- [coin_d4_driver](https://github.com/ROBOTIS-GIT/coin_d4_driver) - [open_manipulator](https://github.com/ROBOTIS-GIT/open_manipulator) - [dynamixel_sdk](https://github.com/ROBOTIS-GIT/DynamixelSDK) - [OpenCR-Hardware](https://github.com/ROBOTIS-GIT/OpenCR-Hardware) diff --git a/turtlebot3_dqn/CHANGELOG.rst b/turtlebot3_dqn/CHANGELOG.rst index b910ed9..560c3e5 100644 --- a/turtlebot3_dqn/CHANGELOG.rst +++ b/turtlebot3_dqn/CHANGELOG.rst @@ -2,6 +2,17 @@ Changelog for package turtlebot3_dqn ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +1.0.2 (2026-01-06) +------------------ +* Fixed a bug in the JSON file where the step parameter was incorrectly named; renamed it to step_counter. +* Changed the system arguments to be passed as ROS parameters for execution. +* Added a use_gpu parameter to allow selection of whether to use GPU. +* Added a model_file parameter to enable loading an existing trained model and continuing training. +* Renamed the load_model variable to use_pretrained_model for clarity. +* Changed model_path from a class variable to a local variable. +* Introduced lazy import for TensorFlow modules. +* Contributors: Hyungyu Kim + 1.0.1 (2025-05-02) ------------------ * Support for ROS 2 Jazzy version diff --git a/turtlebot3_dqn/package.xml b/turtlebot3_dqn/package.xml index 3c7da70..5cc8369 100644 --- a/turtlebot3_dqn/package.xml +++ b/turtlebot3_dqn/package.xml @@ -1,7 +1,7 @@ turtlebot3_dqn - 1.0.1 + 1.0.2 The turtlebot3_dqn package using reinforcement learning with DQN (Deep Q-Learning). @@ -12,11 +12,10 @@ https://github.com/ROBOTIS-GIT/turtlebot3_machine_learning/issues Gilbert ChanHyeong Lee + Hyungyu Kim python3-pip ament_index_python geometry_msgs - python-tensorflow-pip - python3-numpy python3-pyqt5 python3-pyqtgraph rclpy diff --git a/turtlebot3_dqn/saved_model/model1.h5 b/turtlebot3_dqn/saved_model/model1.h5 new file mode 100644 index 0000000..2dbd5eb Binary files /dev/null and b/turtlebot3_dqn/saved_model/model1.h5 differ diff --git a/turtlebot3_dqn/saved_model/model1.json b/turtlebot3_dqn/saved_model/model1.json new file mode 100644 index 0000000..06437af --- /dev/null +++ b/turtlebot3_dqn/saved_model/model1.json @@ -0,0 +1 @@ +{"epsilon": 0.05000073888272134, "step_counter": 84401, "trained_episodes": 800} diff --git a/turtlebot3_dqn/saved_model/stage1_episode600.h5 b/turtlebot3_dqn/saved_model/stage1_episode600.h5 deleted file mode 100644 index fd84e50..0000000 Binary files a/turtlebot3_dqn/saved_model/stage1_episode600.h5 and /dev/null differ diff --git a/turtlebot3_dqn/saved_model/stage1_episode600.json b/turtlebot3_dqn/saved_model/stage1_episode600.json deleted file mode 100644 index 7b3e0ef..0000000 --- a/turtlebot3_dqn/saved_model/stage1_episode600.json +++ /dev/null @@ -1 +0,0 @@ -{"epsilon": 0.1416302983127139} diff --git a/turtlebot3_dqn/setup.py b/turtlebot3_dqn/setup.py index 7cab70f..f1a7e00 100644 --- a/turtlebot3_dqn/setup.py +++ b/turtlebot3_dqn/setup.py @@ -8,20 +8,28 @@ ('Gilbert', 'kkjong@robotis.com'), ('Ryan Shim', 'N/A'), ('ChanHyeong Lee', 'dddoggi1207@gmail.com'), + ('Hyungyu Kim', 'kimhg@robotis.com'), ] authors = ', '.join(author for author, _ in authors_info) author_emails = ', '.join(email for _, email in authors_info) setup( name=package_name, - version='1.0.1', + version='1.0.2', packages=find_packages(), data_files=[ ('share/ament_index/resource_index/packages', ['resource/' + package_name]), ('share/' + package_name, ['package.xml']), ('share/' + package_name + '/launch', glob.glob('launch/*.py')), ], - install_requires=['setuptools', 'launch'], + install_requires=[ + 'setuptools', + 'launch', + 'tensorflow==2.19.0', + 'numpy==1.26.4', + 'scipy==1.10.1', + 'keras==3.9.2', + ], zip_safe=True, author=authors, author_email=author_emails, diff --git a/turtlebot3_dqn/turtlebot3_dqn/dqn_agent.py b/turtlebot3_dqn/turtlebot3_dqn/dqn_agent.py index 69c7f7c..d20ba94 100644 --- a/turtlebot3_dqn/turtlebot3_dqn/dqn_agent.py +++ b/turtlebot3_dqn/turtlebot3_dqn/dqn_agent.py @@ -15,7 +15,7 @@ # limitations under the License. ################################################################################# # -# Authors: Ryan Shim, Gilbert, ChanHyeong Lee +# Authors: Ryan Shim, Gilbert, ChanHyeong Lee, Hyungyu Kim import collections import datetime @@ -31,52 +31,119 @@ from rclpy.node import Node from std_msgs.msg import Float32MultiArray from std_srvs.srv import Empty -import tensorflow -from tensorflow.keras.layers import Dense -from tensorflow.keras.layers import Input -from tensorflow.keras.losses import MeanSquaredError -from tensorflow.keras.models import load_model -from tensorflow.keras.models import Sequential -from tensorflow.keras.optimizers import Adam from turtlebot3_msgs.srv import Dqn -tensorflow.config.set_visible_devices([], 'GPU') - LOGGING = True current_time = datetime.datetime.now().strftime('[%mm%dd-%H:%M]') +_tensorflow = None +_Dense = None +_Input = None +_MeanSquaredError = None +_load_model = None +_Sequential = None +_Adam = None + + +def _import_tensorflow(): + try: + import tensorflow + from tensorflow.keras.layers import Dense + from tensorflow.keras.layers import Input + from tensorflow.keras.losses import MeanSquaredError + from tensorflow.keras.models import load_model + from tensorflow.keras.models import Sequential + from tensorflow.keras.optimizers import Adam + return ( + tensorflow, + Dense, + Input, + MeanSquaredError, + load_model, + Sequential, + Adam + ) + except ImportError as e: + print(f'Error importing TensorFlow: {e}', file=sys.stderr) + print('Please ensure TensorFlow is properly installed.', file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f'Fatal error during TensorFlow import: {e}', file=sys.stderr) + print('This may be due to missing system libraries or incompatible versions.', + file=sys.stderr) + sys.exit(1) + + +def _ensure_tensorflow(): + global _tensorflow, _Dense, _Input, _MeanSquaredError, _load_model, _Sequential, _Adam + if _tensorflow is None: + (_tensorflow, + _Dense, + _Input, + _MeanSquaredError, + _load_model, + _Sequential, + _Adam) = _import_tensorflow() + +def _create_dqn_metric_class(): + _ensure_tensorflow() + base_class = _tensorflow.keras.metrics.Metric -class DQNMetric(tensorflow.keras.metrics.Metric): + class DQNMetric(base_class): - def __init__(self, name='dqn_metric'): - super(DQNMetric, self).__init__(name=name) - self.loss = self.add_weight(name='loss', initializer='zeros') - self.episode_step = self.add_weight(name='step', initializer='zeros') + def __init__(self, name='dqn_metric'): + super(DQNMetric, self).__init__(name=name) + self.loss = self.add_weight(name='loss', initializer='zeros') + self.episode_step = self.add_weight(name='step', initializer='zeros') - def update_state(self, y_true, y_pred=0, sample_weight=None): - self.loss.assign_add(y_true) - self.episode_step.assign_add(1) + def update_state(self, y_true, y_pred=0, sample_weight=None): + self.loss.assign_add(y_true) + self.episode_step.assign_add(1) - def result(self): - return self.loss / self.episode_step + def result(self): + return self.loss / self.episode_step - def reset_states(self): - self.loss.assign(0) - self.episode_step.assign(0) + def reset_states(self): + self.loss.assign(0) + self.episode_step.assign(0) + + return DQNMetric class DQNAgent(Node): - def __init__(self, stage_num, max_training_episodes): + def __init__(self): super().__init__('dqn_agent') + self.declare_parameter('epsilon_decay', 6000) + self.declare_parameter('max_training_episodes', 1000) + self.declare_parameter('model_file', '') + self.declare_parameter('use_gpu', False) + self.declare_parameter('verbose', True) + self.max_training_episodes = self.get_parameter( + 'max_training_episodes' + ).get_parameter_value().integer_value + model_file = self.get_parameter('model_file').get_parameter_value().string_value + use_gpu = self.get_parameter('use_gpu').get_parameter_value().bool_value + self.verbose = self.get_parameter('verbose').get_parameter_value().bool_value + + DQNMetric = _create_dqn_metric_class() + _ensure_tensorflow() + self.tf = _tensorflow + self.Dense = _Dense + self.Input = _Input + self.MeanSquaredError = _MeanSquaredError + self.load_model = _load_model + self.Sequential = _Sequential + self.Adam = _Adam + + if not use_gpu: + self.tf.config.set_visible_devices([], 'GPU') - self.stage = int(stage_num) self.train_mode = True self.state_size = 26 self.action_size = 5 - self.max_training_episodes = int(max_training_episodes) self.done = False self.succeed = False @@ -86,7 +153,9 @@ def __init__(self, stage_num, max_training_episodes): self.learning_rate = 0.0007 self.epsilon = 1.0 self.step_counter = 0 - self.epsilon_decay = 6000 * self.stage + self.epsilon_decay = self.get_parameter( + 'epsilon_decay' + ).get_parameter_value().integer_value self.epsilon_min = 0.05 self.batch_size = 128 @@ -94,39 +163,45 @@ def __init__(self, stage_num, max_training_episodes): self.min_replay_memory_size = 5000 self.model = self.create_qnetwork() - self.target_model = self.create_qnetwork() - self.update_target_model() - self.update_target_after = 5000 - self.target_update_after_counter = 0 - - self.load_model = False + self.use_pretrained_model = bool(model_file) self.load_episode = 0 self.model_dir_path = os.path.join( os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 'saved_model' ) - self.model_path = os.path.join( + model_path = os.path.join( self.model_dir_path, - 'stage' + str(self.stage) + '_episode' + str(self.load_episode) + '.h5' + model_file ) + if self.use_pretrained_model: + self.model.set_weights(self.load_model(model_path).get_weights()) + json_path = model_path.replace('.h5', '.json') + if os.path.exists(json_path): + with open(json_path) as outfile: + param = json.load(outfile) + self.epsilon = param.get('epsilon', self.epsilon) + self.step_counter = param.get('step_counter', self.step_counter) + self.load_episode = param.get('trained_episodes', self.load_episode) + if self.load_episode >= self.max_training_episodes: + self.get_logger().error('Loaded model episode exceeds max training episodes.') + raise ValueError('Loaded model episode exceeds max training episodes.') + else: + self.get_logger().warn( + f'JSON file not found for {model_file}, using default values.' + ) - if self.load_model: - self.model.set_weights(load_model(self.model_path).get_weights()) - with open(os.path.join( - self.model_dir_path, - 'stage' + str(self.stage) + '_episode' + str(self.load_episode) + '.json' - )) as outfile: - param = json.load(outfile) - self.epsilon = param.get('epsilon') - self.step_counter = param.get('step_counter') + self.target_model = self.create_qnetwork() + self.update_target_after = 5000 + self.target_update_after_counter = 0 + self.update_target_model() if LOGGING: - tensorboard_file_name = current_time + ' dqn_stage' + str(self.stage) + '_reward' + tensorboard_file_name = current_time + ' dqn_reward' home_dir = os.path.expanduser('~') dqn_reward_log_dir = os.path.join( home_dir, 'turtlebot3_dqn_logs', 'gradient_tape', tensorboard_file_name ) - self.dqn_reward_writer = tensorflow.summary.create_file_writer(dqn_reward_log_dir) + self.dqn_reward_writer = self.tf.summary.create_file_writer(dqn_reward_log_dir) self.dqn_reward_metric = DQNMetric() self.rl_agent_interface_client = self.create_client(Dqn, 'rl_agent_interface') @@ -156,7 +231,7 @@ def process(self): while True: local_step += 1 - q_values = self.model.predict(state) + q_values = self.model.predict(state, verbose=self.verbose) sum_max_q += float(numpy.max(q_values)) action = int(self.get_action(state)) @@ -183,7 +258,7 @@ def process(self): if LOGGING: self.dqn_reward_metric.update_state(score) with self.dqn_reward_writer.as_default(): - tensorflow.summary.scalar( + self.tf.summary.scalar( 'dqn_reward', self.dqn_reward_metric.result(), step=episode_num ) self.dqn_reward_metric.reset_states() @@ -194,8 +269,8 @@ def process(self): 'memory length:', len(self.replay_memory), 'epsilon:', self.epsilon) - param_keys = ['epsilon', 'step'] - param_values = [self.epsilon, self.step_counter] + param_keys = ['epsilon', 'step_counter', 'trained_episodes'] + param_values = [self.epsilon, self.step_counter, episode] param_dictionary = dict(zip(param_keys, param_values)) break @@ -203,17 +278,21 @@ def process(self): if self.train_mode: if episode % 100 == 0: - self.model_path = os.path.join( - self.model_dir_path, - 'stage' + str(self.stage) + '_episode' + str(episode) + '.h5') - self.model.save(self.model_path) - with open( - os.path.join( + idx = 1 + while True: + model_path = os.path.join( + self.model_dir_path, + f'model{idx}.h5' + ) + json_path = os.path.join( self.model_dir_path, - 'stage' + str(self.stage) + '_episode' + str(episode) + '.json' - ), - 'w' - ) as outfile: + f'model{idx}.json' + ) + if not os.path.exists(model_path): + break + idx += 1 + self.model.save(model_path) + with open(json_path, 'w') as outfile: json.dump(param_dictionary, outfile) def env_make(self): @@ -251,9 +330,9 @@ def get_action(self, state): if lucky > (1 - self.epsilon): result = random.randint(0, self.action_size - 1) else: - result = numpy.argmax(self.model.predict(state)) + result = numpy.argmax(self.model.predict(state, verbose=self.verbose)) else: - result = numpy.argmax(self.model.predict(state)) + result = numpy.argmax(self.model.predict(state, verbose=self.verbose)) return result @@ -280,13 +359,15 @@ def step(self, action): return next_state, reward, done def create_qnetwork(self): - model = Sequential() - model.add(Input(shape=(self.state_size,))) - model.add(Dense(512, activation='relu')) - model.add(Dense(256, activation='relu')) - model.add(Dense(128, activation='relu')) - model.add(Dense(self.action_size, activation='linear')) - model.compile(loss=MeanSquaredError(), optimizer=Adam(learning_rate=self.learning_rate)) + model = self.Sequential() + model.add(self.Input(shape=(self.state_size,))) + model.add(self.Dense(512, activation='relu')) + model.add(self.Dense(256, activation='relu')) + model.add(self.Dense(128, activation='relu')) + model.add(self.Dense(self.action_size, activation='linear')) + model.compile( + loss=self.MeanSquaredError(), + optimizer=self.Adam(learning_rate=self.learning_rate)) model.summary() return model @@ -306,11 +387,11 @@ def train_model(self, terminal): current_states = numpy.array([transition[0] for transition in data_in_mini_batch]) current_states = current_states.squeeze() - current_qvalues_list = self.model.predict(current_states) + current_qvalues_list = self.model.predict(current_states, verbose=self.verbose) next_states = numpy.array([transition[3] for transition in data_in_mini_batch]) next_states = next_states.squeeze() - next_qvalues_list = self.target_model.predict(next_states) + next_qvalues_list = self.target_model.predict(next_states, verbose=self.verbose) x_train = [] y_train = [] @@ -334,8 +415,8 @@ def train_model(self, terminal): y_train = numpy.reshape(y_train, [len(data_in_mini_batch), self.action_size]) self.model.fit( - tensorflow.convert_to_tensor(x_train, tensorflow.float32), - tensorflow.convert_to_tensor(y_train, tensorflow.float32), + self.tf.convert_to_tensor(x_train, self.tf.float32), + self.tf.convert_to_tensor(y_train, self.tf.float32), batch_size=self.batch_size, verbose=0 ) self.target_update_after_counter += 1 @@ -345,13 +426,9 @@ def train_model(self, terminal): def main(args=None): - if args is None: - args = sys.argv - stage_num = args[1] if len(args) > 1 else '1' - max_training_episodes = args[2] if len(args) > 2 else '1000' rclpy.init(args=args) - dqn_agent = DQNAgent(stage_num, max_training_episodes) + dqn_agent = DQNAgent() rclpy.spin(dqn_agent) dqn_agent.destroy_node() diff --git a/turtlebot3_dqn/turtlebot3_dqn/dqn_test.py b/turtlebot3_dqn/turtlebot3_dqn/dqn_test.py index d7742a0..4ead1ad 100755 --- a/turtlebot3_dqn/turtlebot3_dqn/dqn_test.py +++ b/turtlebot3_dqn/turtlebot3_dqn/dqn_test.py @@ -15,7 +15,7 @@ # limitations under the License. ################################################################################# # -# Authors: Ryan Shim, Gilbert, ChanHyeong Lee +# Authors: Ryan Shim, Gilbert, ChanHyeong Lee, Hyungyu Kim import collections import os @@ -25,22 +25,80 @@ import numpy import rclpy from rclpy.node import Node -from tensorflow.keras.layers import Dense -from tensorflow.keras.losses import MeanSquaredError -from tensorflow.keras.models import load_model -from tensorflow.keras.models import Sequential -from tensorflow.keras.optimizers import RMSprop from turtlebot3_msgs.srv import Dqn +_tensorflow = None +_Dense = None +_MeanSquaredError = None +_load_model = None +_Sequential = None +_RMSprop = None + + +def _import_tensorflow(): + """Lazy import TensorFlow and Keras modules.""" + try: + import tensorflow + from tensorflow.keras.layers import Dense + from tensorflow.keras.losses import MeanSquaredError + from tensorflow.keras.models import load_model + from tensorflow.keras.models import Sequential + from tensorflow.keras.optimizers import RMSprop + return ( + tensorflow, + Dense, + MeanSquaredError, + load_model, + Sequential, + RMSprop + ) + except ImportError as e: + print(f'Error importing TensorFlow: {e}', file=sys.stderr) + print('Please ensure TensorFlow is properly installed.', file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f'Fatal error during TensorFlow import: {e}', file=sys.stderr) + print( + 'This may be due to missing system libraries or incompatible versions.', + file=sys.stderr) + sys.exit(1) + + +def _ensure_tensorflow(): + """Ensure TensorFlow is imported and stored in global variables.""" + global _tensorflow, _Dense, _MeanSquaredError, _load_model, _Sequential, _RMSprop + if _tensorflow is None: + (_tensorflow, + _Dense, + _MeanSquaredError, + _load_model, + _Sequential, + _RMSprop) = _import_tensorflow() + class DQNTest(Node): - def __init__(self, stage, load_episode): + def __init__(self): super().__init__('dqn_test') - - self.stage = int(stage) - self.load_episode = int(load_episode) + self.declare_parameter('model_file', '') + self.declare_parameter('use_gpu', False) + self.declare_parameter('verbose', True) + model_file = self.get_parameter('model_file').get_parameter_value().string_value + use_gpu = self.get_parameter('use_gpu').get_parameter_value().bool_value + self.verbose = self.get_parameter('verbose').get_parameter_value().bool_value + + # Lazy import TensorFlow and store as instance variables + _ensure_tensorflow() + self.tf = _tensorflow + self.Dense = _Dense + self.MeanSquaredError = _MeanSquaredError + self.load_model = _load_model + self.Sequential = _Sequential + self.RMSprop = _RMSprop + + if not use_gpu: + self.tf.config.set_visible_devices([], 'GPU') self.state_size = 26 self.action_size = 5 @@ -51,11 +109,11 @@ def __init__(self, stage, load_episode): model_path = os.path.join( os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 'saved_model', - f'stage{self.stage}_episode{self.load_episode}.h5' + model_file ) - loaded_model = load_model( - model_path, compile=False, custom_objects={'mse': MeanSquaredError()} + loaded_model = self.load_model( + model_path, compile=False, custom_objects={'mse': self.MeanSquaredError()} ) self.model.set_weights(loaded_model.get_weights()) @@ -64,21 +122,25 @@ def __init__(self, stage, load_episode): self.run_test() def build_model(self): - model = Sequential() - model.add(Dense( + model = self.Sequential() + model.add(self.Dense( 512, input_shape=(self.state_size,), activation='relu', kernel_initializer='lecun_uniform' )) - model.add(Dense(256, activation='relu', kernel_initializer='lecun_uniform')) - model.add(Dense(128, activation='relu', kernel_initializer='lecun_uniform')) - model.add(Dense(self.action_size, activation='linear', kernel_initializer='lecun_uniform')) - model.compile(loss=MeanSquaredError(), optimizer=RMSprop(learning_rate=0.00025)) + model.add(self.Dense(256, activation='relu', kernel_initializer='lecun_uniform')) + model.add(self.Dense(128, activation='relu', kernel_initializer='lecun_uniform')) + model.add( + self.Dense( + self.action_size, + activation='linear', + kernel_initializer='lecun_uniform')) + model.compile(loss=self.MeanSquaredError(), optimizer=self.RMSprop(learning_rate=0.00025)) return model def get_action(self, state): state = numpy.asarray(state) - q_values = self.model.predict(state.reshape(1, -1), verbose=0) + q_values = self.model.predict(state.reshape(1, -1), verbose=self.verbose) return int(numpy.argmax(q_values[0])) def run_test(self): @@ -120,9 +182,7 @@ def run_test(self): def main(args=None): rclpy.init(args=args if args else sys.argv) - stage = sys.argv[1] if len(sys.argv) > 1 else '1' - load_episode = sys.argv[2] if len(sys.argv) > 2 else '600' - node = DQNTest(stage, load_episode) + node = DQNTest() try: rclpy.spin(node) diff --git a/turtlebot3_machine_learning/CHANGELOG.rst b/turtlebot3_machine_learning/CHANGELOG.rst index 6013291..822a160 100644 --- a/turtlebot3_machine_learning/CHANGELOG.rst +++ b/turtlebot3_machine_learning/CHANGELOG.rst @@ -2,6 +2,17 @@ Changelog for package turtlebot3_machine_learning ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +1.0.2 (2026-01-06) +------------------ +* Fixed a bug in the JSON file where the step parameter was incorrectly named; renamed it to step_counter. +* Changed the system arguments to be passed as ROS parameters for execution. +* Added a use_gpu parameter to allow selection of whether to use GPU. +* Added a model_file parameter to enable loading an existing trained model and continuing training. +* Renamed the load_model variable to use_pretrained_model for clarity. +* Changed model_path from a class variable to a local variable. +* Introduced lazy import for TensorFlow modules. +* Contributors: Hyungyu Kim + 1.0.1 (2025-05-02) ------------------ * Support for ROS 2 Jazzy version diff --git a/turtlebot3_machine_learning/package.xml b/turtlebot3_machine_learning/package.xml index 29e15a8..3e71461 100644 --- a/turtlebot3_machine_learning/package.xml +++ b/turtlebot3_machine_learning/package.xml @@ -1,7 +1,7 @@ turtlebot3_machine_learning - 1.0.1 + 1.0.2 This metapackage for ROS 2 TurtleBot3 machine learning. @@ -12,6 +12,7 @@ https://github.com/ROBOTIS-GIT/turtlebot3_machine_learning/issues Gilbert ChanHyeong Lee + Hyungyu Kim turtlebot3_dqn ament_cmake