nikkie-ftnextの日記

イベントレポートや読書メモを発信

🤗 evaluate 0.4.3は、scikit-learnをインストールする必要があるとき、なぜ非推奨の pip install sklearn を案内するのか?

はじめに

魔瞳の大鷲寮に入りました! nikkieです。

Hugging Face社が開発する、機械学習評価指標のライブラリについて、なぜ非推奨のインストール方法を案内するエラーメッセージが出されるのか調べました。

目次

🤗 evaluate 「you need to install dependencies['scikit-learn'] using 'pip install sklearn'」

  • Python 3.11.8
  • evaluate 0.4.3

Accuracyを算出してみましょう。

>>> import evaluate
>>> metric = evaluate.load("accuracy")
Downloading builder script: 100%|██████████| 4.20k/4.20k [00:00<00:00, 7.01MB/s]
Traceback (most recent call last):

  File "/.../.venv/lib/python3.11/site-packages/evaluate/loading.py", line 265, in _download_additional_modules
    raise ImportError(
ImportError: To be able to use evaluate-metric/accuracy, you need to install the following dependencies['scikit-learn'] using 'pip install sklearn' for instance'

エラーメッセージによると

  • evaluateでaccuracyを算出するには、scikit-learnが必要1
  • pip install sklearn」と案内

しかしながら、scikit-learnのインストールは pip install scikit-learn です。
昔はpip install sklearnでもインストールできましたが、今は非推奨です。

ではなぜ、evaluateは「pip install sklearn」と案内するのでしょうか?

pip install sklearnという案内による混乱と、対応の過程

執筆時点で、evaluateはscikit-learnが必要なときにpip install sklearnと案内するために、混乱があるように見受けられます。

sklearnと表示される箇所は、かつては2箇所あったようです

  • 昔はdependencies['sklearn']だったが、今はdependencies['scikit-learn']に修正された
  • 昔も今もpip install sklearnと案内される2pip install scikit-learnにすべき)

dependencies['sklearn'] -> dependencies['scikit-learn']の修正は、こちらのプルリクのマージによると思われます。

放置気味だったのでしょう3、同様のプルリク(現在はコンフリクト中なものも含む)も見つけました

実装を覗く

エラーメッセージ送出箇所

scikit-learnのインストールを求めるエラーメッセージ送出の実装はこちら(_download_additional_modules()関数)。
https://github.com/huggingface/evaluate/blob/v0.4.3/src/evaluate/loading.py#L256-L269

# Check library imports
needs_to_be_installed = set()
for library_import_name, library_import_path in library_imports:
    try:
        lib = importlib.import_module(library_import_name)  # noqa F841
    except ImportError:
        library_import_name = "scikit-learn" if library_import_name == "sklearn" else library_import_name
        needs_to_be_installed.add((library_import_name, library_import_path))
if needs_to_be_installed:
    raise ImportError(
        f"To be able to use {name}, you need to install the following dependencies"
        f"{[lib_name for lib_name, lib_path in needs_to_be_installed]} using 'pip install "
        f"{' '.join([lib_path for lib_name, lib_path in needs_to_be_installed])}' for instance'"
    )

needs_to_be_installed(set)に要素があるとき、例外(ImportError)が送出されます。
エラーメッセージのf-stringのうち

  • nameはこの関数(_download_additional_modules())の引数
    • 実際のエラーメッセージ(evaluate-metric/accuracy)から、metricのnameと思われます
  • dependenciesに続く部分はリスト
  • pip installの部分はリストの要素をjoinした文字列

です。

このリストですが、内包表記で作られていて、大元はneeds_to_be_installedですね。
needs_to_be_installedには要素2のタプルが入っています

needs_to_be_installed: set[tuple[str, str]] = set()

例えばsklearnがimportできない場合4、集合needs_to_be_installedには("scikit-learn", "sklearn")が追加されます。
このためエラーメッセージは

you need to install the following dependencies['scikit-learn'] using 'pip install sklearn' for instance'

となります(余談ですが、メッセージ末尾に不要なシングルクォートがあるのですね)

ですが、案内すべきはpip install scikit-learnですから、

you need to install the following dependencies['scikit-learn'] using 'pip install scikit-learn' for instance'

としたく、try ... except ... のexceptの条件式5を使っているところは

library_import_name = "scikit-learn" if library_import_name == "sklearn" else library_import_name
+library_import_path = "scikit-learn" if library_import_name == "sklearn" else library_import_path

のような修正が必要そうです6

sklearnという文字列はどこから来るのか

上記のコードのlibrary_importsの要素が("sklearn", "sklearn")となっている理由も見ていきます。
_download_additional_modules()関数 https://github.com/huggingface/evaluate/blob/v0.4.3/src/evaluate/loading.py#L212-L214

def _download_additional_modules(
    name: str, base_path: str, imports: Tuple[str, str, str, str], download_config: Optional[DownloadConfig]
) -> List[Tuple[str, str]]:
    # library_importsに関係する箇所のみ抜き出し
    library_imports = []
    for import_type, import_name, import_path, sub_directory in imports:
        if import_type == "library":
            library_imports.append((import_name, import_path))  # Import from a library
            continue
        
        # 省略
    
    # Check library imports (*すでに見た箇所*)

関数_download_additional_modules()の引数importsのうち、import_type"library"の要素をlibrary_importsとしています。
では、importsにはどのような値が渡るのでしょうか。

エラーメッセージを参考にすると呼び出しはこちら。
https://github.com/huggingface/evaluate/blob/v0.4.3/src/evaluate/loading.py#L489

class HubEvaluationModuleFactory(_EvaluationModuleFactory):
    def get_module(self) -> ImportableModule:
        # 省略

        # get script and other files
        try:
            local_path = self.download_loading_script(revision)
        except FileNotFoundError as err:
            # 省略

        imports = get_imports(local_path)
        local_imports = _download_additional_modules(
            name=self.name,
            base_path=hf_hub_url(path=self.name, name="", revision=revision),
            imports=imports,
            download_config=self.download_config,
        )
        # 省略

メソッド名とデバッガで見た変数の値から判断すると、evaluate.load("accuracy")したときに、metricを実装したPythonファイルをダウンロードするようです(「Downloading builder script」出力)。
https://github.com/huggingface/evaluate/blob/v0.4.3/metrics/accuracy/accuracy.py と思しきファイルがローカルPCにキャッシュされていました。

関数get_imports()は、metricの実装から正規表現import ...from ... import ...の行を取り出しています。
https://github.com/huggingface/evaluate/blob/v0.4.3/src/evaluate/loading.py#L139

相対importの考慮や

The import starts with a '.', we will download the relevant file

evaluate独自仕様と思われる「From:」で始めるコメントの処理がありました。

The import has a comment with 'From:', we'll retrieve it from the given url

scikit-learnの場合はこの分岐を通ると思われます
https://github.com/huggingface/evaluate/blob/v0.4.3/src/evaluate/loading.py#L207

imports.append(("library", match.group(2), match.group(2), None))
# get_imports() は imports を返します

accuracyの実装にはscikit-learnを使った行がありますから、

from sklearn.metrics import accuracy_score

match.group(2)として from ... import ...の(先頭の.を除いて)最初の.まで(すなわち"sklearn")。
よって、get_imports()の返り値に ("library", "sklearn", "sklearn", None) と含まれます。
scikit-learnというPyPIの登録名ではなく、コード中での名前sklearn)となるわけですね。

すでに見た_download_additional_modules()関数では、importしてみてできなければImportErrorを送出するという実装ですから、エラーメッセージでだけscikit-learnという名前に変える必要があるわけですね

終わりに

🤗 evaluateで見かけた pip install sklearn という非推奨の案内がどこから来るかを理解しました。

  • evaluate.load("accuracy")と呼び出したとき、accuracyの実装のPythonファイルをダウンロードしている
  • accuracyの実装のPythonファイルから正規表現で依存ライブラリを取り出す
    • sklearn表記
  • importしてみてできない場合はImportErrorを送出
    • エラーメッセージにあたりsklearnscikit-learn表記に変えようとしているが、実装が不十分

エラーメッセージの表示をどう直すのがよさそうか、私の中では整理できました。
いっちょプルリク出してきます💪

と思ったのですが、このプルリクをマージできるように動くだけでよさそうということに気づきました。ありがたや〜


  1. scikit-learnに依存する実装です。 https://github.com/huggingface/evaluate/blob/v0.4.3/metrics/accuracy/accuracy.py#L17
  2. 重複気味ですが、別のissueも立っています
  3. 放置気味だったためにプルリクが重複した私の経験から
  4. importlib.import_module でモジュールをimportしています
  5. 条件式の参考
  6. もし Fix: message for lack of sklearn by ArkiZh · Pull Request #614 · huggingface/evaluate · GitHub が代わりにマージされていたら、「you need to install the following dependencies['sklearn'] using 'pip install scikit-learn' for instance'」と表示されていて、現状よりも混乱が少なかったと思われます