Google Colab(3.10)で古いバージョンのPython(3.8)を動かす

## はじめに

(2024-01-22追記)
読者から、この手順でもバージョン切り替えができないとの質問があり、試したところその通りでした。
記事執筆時点からColab側の環境が変わり、今は動かない状態になっています。対応手段がないか、現在調査中です。
やりかたが見つかり次第、記事を改訂しますので、それまでお待ちいただければと思います。

---

「Pythonで儲かるAIをつくる」などの著者の赤石です。
私の本は基本的にPythonの実習コード付きです。実習コードの前提環境をどうするかが、常に大きな問題なのですが、最近の本ではすっかり**Google Colab**に頼っています。
Google Colabは、gmailのアカウントさえ持っていれば、セットアップ作業ゼロで、即Jupter + Pythonの環境が使える点が圧倒的に便利だからです。
一方で、要注意点もあります。Google Colabは各種ライブラリの最新バージョンを常に反映しています。それはそれでありがたいことなのですが、出版時とライブラリのバージョンが変わると、サンプルプログラムの挙動が変わることがあるのです。
このような事象が見つかった場合は、読者のためにできる限りサンプルコードに修正を加えて本と同じ動きを保つようにしています。

しかし、「Pythonで儲かるAIをつくる」の5.5節で今回起きた事象は、かなり複雑でした。
乱数の種も固定値で指定してもクラスタリングの結果が、紙面と変わってしまうというのが起きている事象であり、直接の原因はscikit-learnのバージョンに依存していることまではわかりました。
いつもだと、notebookの冒頭に

```
!pip install scikit-learn==0.23.2
```

行を追加し、「バージョンを下げた後でカーネルを再起動してください」というガイドを入れることで解決するのですが、今回は上のバージョンダウングレードのコマンドを実行すると、エラーになってしまったのです。
このエラーの根本原因は、ベースのPythonのバージョンにあり、**現行のColabのPython 3.10をPython 3.8に下げないとscikit-learnのダウングレードができない**、これをなんとか実行したいというのが今回のお題になります。

かなり難しいお題だったのですが、いろいろと試行錯誤した結果、なんとか動く状態のものをつくることができたので、当記事にてその手順を説明します。
Google社の公式サポートはない、かなり危なっかしい使い方ですが、同じようなニーズがある方は、個人の責任の上で試してもらえると参考になるかもしれないです。

## 基本的な方針
Jupyterは、Webの画面を提供しているフロントのサービスの背後に「カーネル」があります。デフォルトで動いている「カーネル」と別のカーネルを作り、ここにUIの接続先を付け変えると、やりたいことができるようになります。
しかし、既存のカーネルも、同じVM上で動いています。まったく別のPython環境をつくり、既存環境と競合が起きないように注意深く行う必要があります。
そこで今回は、プログラムの衝突が起きないよう``/usr/local_py38``というパスに、新しいPython関係のプログラムを導入する方針としました。
ネットで同種のことをやっている事例があり、そこではpipでなくcondaを使って新環境を構築していたので、これから紹介する記事も同じ方針で実装しています。

## 導入手順
ここからは、実際に動くサンプルコードの解説をします。
コード全体は、https://github.com/makaishi2/samples/blob/main/notebooks/colab_python_downgrade.ipynb にアップしてあります。
また、冒頭で説明した「Pythonで儲かるAIをつくる」の5.5節に関しては　https://github.com/makaishi2/profitable_ai_book_info/blob/master/notebooks/ch05_05_clustering_20231105_colab.ipynb にこの問題に対応した版のnotebookをアップしました。

### 環境変数設定
最初に環境変数の初期設定をします。
新しい導入先である``/usr/local_py38``配下のコマンドやライブラリが、呼び出し可能になるように、PATHとsys.pathに修正をかけます。

```
import os
path_org = os.environ['PATH']
path_38 = '/usr/local_py38'
path_38bin = f'{path_38}/bin'
os.environ['PATH'] = f'{path_38bin}:{path_org}'

import sys
sys.path.append(f'{path_38}/lib/python3.8/site-packages')
```

### miniconda導入
minicondaの導入をします。導入先は``/usr/local_py38``配下とします。
各コマンドのコンソール出力がすべてnotebookにでてくるのがうるさいので、``tail -n 1``でフィルターしています。
各処理がうまくいかなくて問題判別をしたい場合は、このフィルターを取ってください。

```
!wget -O mini.sh https://repo.anaconda.com/miniconda/Miniconda3-py38_4.8.2-Linux-x86_64.sh | tail -n 1
!chmod +x mini.sh
!bash ./mini.sh -b -f -p $path_38 | tail -n 1
```

### 結果確認
ここまでの導入がうまくいった場合はcondaコマンドが使えるはずです。また、pipコマンドも今回導入したものが呼ばれるようになるはずです。この2つのことを確認します。

```
# 結果確認
!which conda
!which pip
```

次の結果になっていればOKです。

```
/usr/local_py38/bin/conda
/usr/local_py38/bin/pip
```

### ライブラリ追加導入
condaコマンドを使って、カーネルを新規に立ち上げるのに必要なライブラリを追加導入します。
ネットから取ってきた情報に基づいていますが、なくていいものもあるかもしれないです。

```
!conda install -q -y conda python=3.8 --prefix $path_38 | tail -n 1
!conda install -q -y jupyter --prefix $path_38 | tail -n 1
!conda install -q -y google-colab -c conda-forge --prefix $path_38 | tail -n 1
```

### カーネル追加
今回の手順の核心部分である「カーネル追加」です。
具体的には``ipykernel``というコマンド？を使います。
カーネルが導入できているかの確認は``jupyter-kernelspec list``というコマンドで行います。

```
!python -m ipykernel install --name "py38" --user
# 結果確認
!jupyter-kernelspec list
```

このセルの実行結果は次のようになればOKです。

```
Installed kernelspec py38 in /root/.local/share/jupyter/kernels/py38
Available kernels:
  py38       /root/.local/share/jupyter/kernels/py38
  python3    /usr/local_py38/share/jupyter/kernels/python3
  ir         /usr/local/share/jupyter/kernels/ir
```

結果出力のうち、``python3``が元々あったPython用のカーネル(3.10)、``py38``が今回追加したカーネルです。
``ir``はおそらくR用のものではないかと思います。

### カーネル切替え
これですべての準備は整ったので、いよいよカーネルの切り替えを行います。
ここは、今回の検証で最も苦労したところであり(リストに出ても、切り替えがうまくいかないことがよくあった)、今でも実施するときはドキドキします。
ここだけは、恐らくコマンドで実施できなくて、ColabのUIを使って行います。
画面上部のメニューから「ランタイム」「ランタイムのタイプを変更」を選択します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206130/192ae81f-7d92-af26-6b5b-1fd5785d10a4.png" width=500>

上のようなパネルが表示されます。
①　ランタイムのタイプの直下のドロップダウンをクリックします
②　選択肢にpy38があるはずなので、それを選択します
③　py38が選ばれた状態で、画面右下の「保存」ボタンをクリックします

切替えがうまくいくと、画面下部のステータス表示が下のようになります。
逆に「接続中」のままステータスが進まない場合は、今までの手順のどこかが間違っていた可能性があるので、手順を見直してください。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206130/18bc0e86-ab6f-4964-52dc-566ac6b78fc6.png" width=600>

念のため、次のセルで新しいカーネルの動作テストをします。

```
# 新カーネルの動作テスト
1+1
```

セル実行の順番が下のように「1」　に戻っていたら、カーネルの切り替えに成功しています。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206130/1be71a4f-e92e-13b4-c52e-5e67a2087a63.png" width=500>

### 環境変数再設定
新しいカーネルに向けて、環境変数の再設定をします。

最初に``sys.path``の確認をします。今回は、``/usr/local_py38``配下にプログラムとライブラリ一式を導入しているので、　sys.pathにこの情報が含まれている必要があります。
結論からいうと、この点は初期状態ですでに正しくなっているようです。
次のコードでは、ついでに新しいカーネルのPythonバージョンの確認もしています。

```
# sys.pathの設定を確認
# 次の設定は不要
# sys.path.append(f'{path_38}/lib/python3.8/site-packages')

import sys
print(f'Python Version: {sys.version}')
for path in sys.path:
    print(path)
```

こんな結果になるはずです。

```
Python Version: 3.8.18 (default, Sep 11 2023, 13:20:55) 
[GCC 11.2.0]
/content
/env/python
/usr/local_py38/lib/python38.zip
/usr/local_py38/lib/python3.8
/usr/local_py38/lib/python3.8/lib-dynload

/usr/local_py38/lib/python3.8/site-packages
```

Pythonのバージョンは3.8.18、ライブラリは``/usr/local_py38``配下のディレクトリのみとなっており、意図した設定になっていることがわかります。

次はPATHの設定です。PATHに関しては、カーネルを新しくしたタイミングで``/usr/local_py38/bin``の設定がなくなってしまっているので、改めて設定をします。

```
import os
path_org = os.environ['PATH']
path_38 = '/usr/local_py38'
path_38bin = f'{path_38}/bin'
os.environ['PATH'] = f'{path_38bin}:{path_org}'
# 設定確認
print(os.environ['PATH'])
```

こんな結果になるはずです。

```
/usr/local_py38/bin:/opt/bin:/usr/local/nvidia/bin:/usr/local/cuda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/tools/node/bin:/tools/google-cloud-sdk/bin
```

### ライブラリ追加導入#2
ここまでたどり着けば、あとは、いつものやりかたでライブラリの追加導入をします。
今回、肝となるscikit-learnに関しては、このタイミングでバージョン番号付きで導入します。
``--prefix``オプションで導入先パス指定をする点に注意してください。

```
!conda install -q -y scikit-learn==0.23.2 --prefix $path_38 | tail -n 1
!conda install -q -y matplotlib --prefix $path_38 | tail -n 1
```

### バージョン確認
古い版のscikit-learnの動作確認をします。
scikit-learnのバージョンが意図した値になっていることと、今回の実習で使いたいKMeansのモデルが正しくimportできることを確認します。後者は、今回の検証で条件によりうまくいかないことがあったので、テストケースに追加しました。

```
import sklearn
print(sklearn.__version__)
from sklearn.cluster import KMeans
```

## おわりに
今回の検証のきっかけであった「PythonでもうかるAIをつくる」5.5節の実習については、これでめでたく紙面の動作を再現できるようになりました。
しかし、あまりに手順が複雑で、Google Colabを標準的な実行環境に採用した当初の目的から遠いところに来てしまったとも思います。Pythonのバージョン別のカーネルを用意していただくなど、Google社の方で、抜本的な解決策を検討いただけるとありがたいかなと思う今日このごろでした。