
작년 말쯤 Optuna라는 오픈소스에 컨트리뷰션 하고 메인 브랜치에 머지된 경험이 있다. 비록 늦기는 했지만, 지금이라도 그때의 기억을 되살려 에러를 처리하고 코드리뷰 과정을 거쳐 소스가 반영되기까지 과정을 기록해 본다.
계기
이전에 진행하던 머신러닝 프로젝트를 수행하며 Optuna를 사용해 하이퍼파라미터 튜닝을 위한 실험 환경을 구성한 경험이 있다. Optuna를 조금씩 사용해 보고 공식 문서도 읽어보다 issue 페이지까지 보게 되었는데, 간단히 해결할 수 있어 보이는 이슈가 눈에 띄었다.

이슈에 달려있는 라벨도 친절하게 도전 욕구를 자극하는 good first issue 가 달려있었다.
나처럼 오픈소스에 기여하는데 능숙하지 않은 사람도 good first issue, contribution welcome과 같은 라벨이 달려있는 이슈는 큰 어려움 없이 기여할 수 있다.

해당 이슈를 해결하기 위한 작업을 하고 있다는 코멘트를 남기고 컨트리뷰션을 시작했다!

Optuna는 뭐 하는 프로젝트?
Optuna는 하이퍼파라미터 최적화를 위한 오픈소스 프레임워크이다. 주로 머신러닝 모델을 실험할 때 사용하는데, 찾고자 하는 하이퍼파라미터의 범위(search space)를 지정하면 모델을 학습시키고 최적화된 파라미터를 찾는 과정을 자동화시켜 준다.
이외에도 여러 실험에 대한 병렬 수행, 시각화 기능등 하이퍼파라미터 최적화를 위한 여러 부가기능을 제공하는 오픈소스 프로젝트이다.
import optuna
def objective(trial):
x = trial.suggest_float('x', -10, 10)
return (x - 2) ** 2
study = optuna.create_study()
study.optimize(objective, n_trials=100)
study.best_params # E.g. {'x': 2.002108042}
샘플 코드
공식사이트 https://optuna.org/
Github https://github.com/optuna/optuna
이슈에 대한 분석
https://github.com/optuna/optuna/issues/4136
PartialFixedSamp≤r does not handle No≠ correctly · Issue #4136 · optuna/optuna
Expected behavior In the following codes, "x" should be None. But sometimes 0 is sampled. import optuna def objective(trial): x = trial.suggest_categorical("x", (None, 0)) retur...
github.com
해당 이슈의 내용은 다음과 같다
import optuna
def objective(trial):
x = trial.suggest_categorical("x", (None, 0))
return 0
tpe = optuna.samplers.TPESampler()
sampler = optuna.samplers.PartialFixedSampler({"x": None}, tpe)
study = optuna.create_study(sampler=sampler)
study.optimize(objective, n_trials=10)
위 코드에서 PartialFixedSampler를 사용해 x의 탐색 변수를 고정(line 8)했음에도 불구하고 '0'이 샘플링된다는 에러였다.
PartialFixedSampler는 특정 변수의 탐색 범위를 고정한다. 해당 이슈에서는 None, 0으로 이루어진 탐색범위(line 4)에서 None값으로 x변수의 탐색범위를 고정했으므로 '0'이 샘플링되어서는 안 된다.
이슈를 올린 Maintainer가 친절하게 문제 되는 코드의 위치까지 알려주었다.
# If param_name isn't in self._fixed_params.keys(), param_value is set to None.
param_value = self._fixed_params.get(param_name)
if param_value is None:
# Unfixed params are sampled here.
return self._base_sampler.sample_independent(
study, trial, param_name, param_distribution
)
else:
# Fixed params are sampled here.
# Check if a parameter value is contained in the range of this distribution.
param_value_in_internal_repr = param_distribution.to_internal_repr(param_value)
contained = param_distribution._contains(param_value_in_internal_repr)
if not contained:
warnings.warn(
f"Fixed parameter '{param_name}' with value {param_value} is out of range "
f"for distribution {param_distribution}."
)
return param_value
문제점
param_value = self._fixed_params.get(param_name)
이 부분이 문제이다. python dictionary에서 . get(KEY_NAME)은 key 값에 따라 value를 돌려주는 형태로 작동한다. key에 매칭되는 value 가 존재하지 않으면 None을 리턴하기 때문에 key의 존재 여부를 확인할 수 있다.
만일 어떤 key 값에 Value에 None이 들어있을 경우 해당 로직에서는 key가 없어서 None이 리턴되었는지, Value 값에서 None 값이 넘어왔는지 알 수 없기 때문에 두 경우가 구분되지 않는다.
문제점을 파악했으니 코드를 수정하고 PR을 날리면 된다!
프로젝트 컨트리뷰션 준비
1. Fork
포크 버튼을 눌러 해당 repository를 내 repository로 가져온다.


2. 컨트리뷰션 가이드라인 확인
https://github.com/optuna/optuna/blob/master/CONTRIBUTING.md
GitHub - optuna/optuna: A hyperparameter optimization framework
A hyperparameter optimization framework. Contribute to optuna/optuna development by creating an account on GitHub.
github.com
컨트리뷰션 가이드라인을 확인하는 과정은 필수이다. 가이드라인에는 이슈를 남기는 규칙, 컨트리뷰션 방법, 프로젝트를 세팅하는 방법까지 알려주기 때문에 꼭 확인을 해봐야 한다.
3. 프로젝트 세팅
3.1 가상환경 생성
python 패키지의 의존성을 독립적으로 관리하기 위해 optuna를 위한 python 3.8 버전의 가상환경을 만들어줬다.
conda create -n optuna python=3.8
3.2 clone
fork 한 레포지토리를 로컬 환경으로 clone 한다.
git clone https://github.com/halucinor/optuna.git
3.3 패키지 설치
프로젝트에서 요구하는 패키지를 모두 설치한다. Contribution 가이드라인에 안내된 것처럼 다음과 같은 명령어를 통해 모든 의존성을 설치할 수 있다.
Documentation 관련된 패키지는 설치하지 않았다(선택)
# 패키지가 설치된 경로로 이동
cd optuna
# 기본 패키지 설치
pip install -e .
# 코딩 스타일, 포맷, 타입힌트 관련 패키지 설치
# Install auto-formatters.
$ pip install ".[checking]"
# 단위테스트 패키지
# Install required packages to test all modules without visualization and integration modules.
pip install ".[test]"
코드 수정 및 MR
코드를 다음과 같이 수정했다. key가 없을 경우와 value 가 None일 경우를 구분하기 위해 코드를 수정했고 테스트코드까지 작성해 코드가 정상적으로 작동하는 것을 확인할 수 있었다.
코드 작성
# If param_name isn't in self._fixed_params.keys(), param_value is set to None.
# And if param_value is `None` itself it should be `None`
if param_name not in self._fixed_params.keys():
# Unfixed params are sampled here.
return self._base_sampler.sample_independent(
study, trial, param_name, param_distribution
)
else:
# Fixed params are sampled here.
# Check if a parameter value is contained in the range of this distribution.
param_value = self._fixed_params.get(param_name)
param_value_in_internal_repr = param_distribution.to_internal_repr(param_value)
contained = param_distribution._contains(param_value_in_internal_repr)
테스트 코드
def test_fixed_none_value_sampling() -> None:
def objective(trial: Trial) -> float:
trial.suggest_categorical("x", (None, 0))
return 0.0
tpe = optuna.samplers.TPESampler()
with warnings.catch_warnings():
warnings.simplefilter("ignore", optuna.exceptions.ExperimentalWarning)
# In this following case , "x" should sample only `None`
sampler = optuna.samplers.PartialFixedSampler(fixed_params={"x": None}, base_sampler=tpe)
study = optuna.create_study(sampler=sampler)
study.optimize(objective, n_trials=10)
for trial in study.trials:
assert trial.params["x"] is None
테스트
코드가 정상적으로 작동하는지 테스트한다.
테스트코드를 작성하고 이를 검증하는 것은 내 코드의 신뢰성을 확보하는 측면에서 매우 중요하다.

PR 날리기!
해당 오픈소스의 정해진 양식에 따라 Pull Requset의 내용을 작성한다.

코드리뷰 그리고 수정
Pull Request가 올라가면 메인테이너의 코드리뷰 이후 해당 PR의 반영 여부가 결정된다.

메인테이너와 몇 번의 핑퐁을 거치며 코드를 수정하였다. 조금 더 직관적으로, 군더더기가 없는 코드를 원했기 때문에 불필요한 코드를 좀 더 깔끔하게 만들었다.
if param_name not in self._fixed_params:
# Unfixed params are sampled here.
return self._base_sampler.sample_independent(
study, trial, param_name, param_distribution
)
else:
# Fixed params are sampled here.
# Check if a parameter value is contained in the range of this distribution.
param_value = self._fixed_params[param_name]
param_value_in_internal_repr = param_distribution.to_internal_repr(param_value)
contained = param_distribution._contains(param_value_in_internal_repr)
if not contained:
warnings.warn(
f"Fixed parameter '{param_name}' with value {param_value} is out of range "
f"for distribution {param_distribution}."
)
return param_value
마무리
코드리뷰에 맞춰 코드를 수정하거나 리뷰어의 질문에 대답하는 과정을 마무리하면 해당 코드가 메인테이너에게 승인이 되고 main 브랜치로 머지된다. 👏👏👏

첫 번째 오픈소스 컨트리뷰션을 무사히 마칠 수 있었다. 아주 오래전부터 오픈소스에 기여해 보는 경험을 해보고 싶다는 생각을 하고 있었는데 마음처럼 쉽지가 않았다. 적절한 오픈소스를 찾기도 힘들었고, 기여하고 싶은 오픈소스는 기능이 너무 복잡해 기여할 수 있는 기능을 찾기도 어려웠다. 😥
마침 관심이 있던 오픈소스의 어렵지 않은 이슈가 있어서 기회를 잘 잡을 수 있었던 것 같다.
깃헙 프로필에 기여한 프로젝트의 배지가 달린 것도 소소한 기쁨이지만, 오픈소스에 기여하는 과정에서 프로젝트를 분석/수정하고 코드를 관리하는 메인테이너와 리뷰를 주고받으며 코드를 개선해 나간 값진 경험을 해볼 수 있었다.
'끄적끄적' 카테고리의 다른 글
[회고] 2024년 회고 (2) | 2024.12.22 |
---|---|
[후기] 글또 9기를 되돌아보며... (2) | 2024.05.12 |
[후기] 글또 8기를 마무리하며 (0) | 2023.07.16 |
[회고] 2023 상반기 인턴 경험 (1) | 2023.05.12 |
습관을 형성하고 나쁜 습관을 끊어내는 방법(by 앤드류 휴버맨) (0) | 2023.04.30 |