TensorFlowを利用するWSGIサーバの構築に向けたPython3環境の構築

# 背景

ChatGPTには、画像アップロード機能が加わり、画像解析を行う機能が追加されたことを知ることになった。

画像ストアサービス【<a href='https://github.com/hashimotonet/PhotoGalleryService'>PhotoGallery</a>（<a href="https://osm.yokohama">osm.yokohama</a>）】を運営する身としては、垂涎ものである機能と思い、早速WebAPI経由で接続をすることを試したくなった。

そんな中、ChatGPTの中で、文章末尾にあるソースの提示を受ける。ありがたや、ありがたや。

## 前提
OS: Amazon Linux 2
Python: version python-3.9.18
Apache Web Server version: Apache/2.4.59

#### TensorFlowは、以下のスレッドで問題に上がっているようなことがあるため、Pythonバージョンを指定する必要がある
https://github.com/pybind/pybind11/issues/3115#issuecomment-1923476286

また、pyenv からの実行環境のインストールは、OpenSSL関連でつまづいたので、ソースからインポートする必要があった。

### 必要なライブラリをまずインストール
```
$ sudo yum install libffi-devel
$ sudo yum install perl
$ sudo yum install perl-IPC-Cmd
$ sudo yum install perl-Test-Simple
$ sudo yum install zlib-devel
$ sudo yum install bzip2-devel
$ sudo yum install ncurses-devel
$ sudo yum install libffi-devel
$ sudo yum install libuuid libuuid-devel
$ sudo yum install sqlite-devel
$ sudo yum install readline-devel
$ sudo yum install tk-devel
```

### opensslのインストール
リンク先を参考に、opensslの適切なバージョンをインストールする。

https://qiita.com/KBT777/items/2ae15faa4b8c7101d6f1#openssl%E3%82%92%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E3%81%99%E3%82%8B

### openssl環境
```
$ openssl version -a
```
当方環境の場合
```
OpenSSL 3.1.2 1 Aug 2023 (Library: OpenSSL 3.1.2 1 Aug 2023)
built on: Mon Jun 24 18:31:19 2024 UTC
platform: linux-x86_64
options:  bn(64,64)
compiler: gcc -fPIC -pthread -m64 -Wa,--noexecstack -Wall -O3 -DOPENSSL_USE_NODELETE -DL_ENDIAN -DOPENSSL_PIC -DOPENSSL_BUILDING_OPENSSL -DNDEBUG
OPENSSLDIR: "/usr/local/ssl"
ENGINESDIR: "/usr/local/lib64/engines-3"
MODULESDIR: "/usr/local/lib64/ossl-modules"
Seeding source: os-specific
CPUINFO: OPENSSL_ia32cap=0xfffa3203478bffff:0x7a9
```

インストール時に必要な設定（前途のopensslコマンドの出力などに合わせる）
```
$ export CFLAGS="-I/usr/include/openssl -L/usr/local/src/bzip2-1.0.8"
$ export LDFLAGS="-L/usr/local/lib64 -L/usr/local/lib -L/usr/local/lib64 -L/usr/share/tcl8.5/sqlite3 -L/usr/local/lib64"
$ export LD_RUN_PATH=/usr/local/lib64:/usr/local/lib:/usr/lib:/usr/lib64
$ export LD_LIBRARY_PATH=/usr/local/lib64:/usr/local/lib:/usr/lib:/usr/lib64
$ export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig
$ export OPENSSL_LDFLAGS=-L/usr/include/openssl
$ export OPENSSL_LIBS=-l/usr/lib64/openssl
$ export OPENSSL_INCLUDES=-I/usr/include/openssl
```

ディレクトリを移動し、以下を実行。
```
$ cd /usr/local/src
$ sudo wget https://www.python.org/ftp/python/3.9.18/Python-3.9.18.tgz
$ sudo tar xvzf Python-3.9.18.tgz
$ cd Python-3.9.18
```

### [Modles/Setup]ファイルの編集
```
$ vi Modules/Setup
```
206～209行目をコメント解除（行番号はオプションで表示）
```Modules/Setup
204:# Socket module helper for SSL support; you must comment out the other
205:# socket line above, and possibly edit the SSL variable:
206:SSL=/usr/local/ssl
207:_ssl _ssl.c \
208:    -DUSE_SSL -I$(SSL)/include -I$(SSL)/include/openssl \
209:    -L$(SSL)/lib -lssl -lcrypto
```


ここまでで何度かつまづいている場合には、以下を実行しましょう。

> $ sudo make clean

## 実行
```
$ ./configure \
--with-ensurepip \
--enable-shared \
--enable-ipv6 \
--with-openssl=/usr/local/ssl \
--enable-optimizations \
--enable-threads \
--prefix=/usr/local/python-3.9.18 \
--with-system-ffi=/usr/local/lib64 \
--enable-loadable-sqlite-extensions \
--with-openssl-rpath=auto \
--with-openssl=/usr/local/src/openssl-3.1.2
$ sudo make -j 8
$ sudo make altinstall
```

また下記を行う
```
$ ln -s /usr/local/python-3.9.18/lib/libpython3.9.so.1.0 /usr/local/lib64/libpython3.9.so.1.0
$ export LD_LIBRARY_PATH=/usr/local/lib64:/usr/lib64
```


## 結果としてのPyCMethod_Newに関する定義
```
$ readelf -s  /usr/local/src/Python-3.9.18/libpython3.9.so |  grep "PyCMethod"
```
```
   419: 0000000000227a70   775 FUNC    GLOBAL DEFAULT   11 PyCMethod_New
  1657: 00000000007c8f00   408 OBJECT  GLOBAL DEFAULT   23 PyCMethod_Type
  9888: 00000000008c2ba0    48 OBJECT  LOCAL  DEFAULT   24 __gcov0.PyCMethod_GetClas
  9889: 00000000008c2b88     8 OBJECT  LOCAL  DEFAULT   24 __gcov7.PyCMethod_GetClas
  9896: 00000000008c2a00     8 OBJECT  LOCAL  DEFAULT   24 __gcov7.PyCMethod_New
  9897: 00000000008c2a20   160 OBJECT  LOCAL  DEFAULT   24 __gcov0.PyCMethod_New
  9949: 00000000007c8520    72 OBJECT  LOCAL  DEFAULT   23 __gcov_.PyCMethod_GetClas
  9953: 00000000007c83a0    72 OBJECT  LOCAL  DEFAULT   23 __gcov_.PyCMethod_New
 29548: 0000000000227750   202 FUNC    LOCAL  DEFAULT   11 PyCMethod_GetClass
 30142: 0000000000227a70   775 FUNC    GLOBAL DEFAULT   11 PyCMethod_New
 31269: 00000000007c8f00   408 OBJECT  GLOBAL DEFAULT   23 PyCMethod_Type
```

### インストールされたtensor関連ライブラリはこちら
```
$ pip list | grep tensor
tensorboard                  2.16.2
tensorboard-data-server      0.7.2
tensorflow                   2.16.1
tensorflow-estimator         2.15.0
tensorflow-io-gcs-filesystem 0.37.0
tensorrt                     8.5.2.2
```

## 参考

#### AWS EC2(Amazon Linux 2)サーバー に Apache2.4, Python3, wsgiの実行環境を構築する
https://qiita.com/uneri/items/84856312c8e94a6748bf

#### Python3.xをインストール（ソースからビルド）for RockyLinux 9.x
https://qiita.com/shadowhat/items/025c49df5e45d6c1b665


## 実行サーバのソース

```
$ python3 App.py
```

Pythonがインストールできており、ライブラリが完備されていれば*上記コマンドでも、実行は可能だ。ただし、実行ログで

:::note warn
開発のみに使用すべきであって、製品として使用するにはWSGI環境で実行してください
:::

と言われるので、リンク先のサイトを参考に、環境を構築することとしてください。

最後に、提供を受けたものですが、アップロード対象サーバ及びGPT-4 に接続し、画像解析を行う（開発中ですが...）ソースを掲載します。

```python:App.py
#!/usr/local/python-3.9.18/bin/python3.9 
# -*- coding: utf-8 -*-
import sys
sys.path.append('')
sys.path.append('/home/ec2-user/.local/lib/python3.9/site-packages') 

import tensorflow as tf
from flask import Flask, request, jsonify
import os
from PIL import Image
import numpy as np

app = Flask(__name__)

# 下記と同名のフォルダを、実行環境と同一の階層に作成する
UPLOAD_FOLDER = 'uploads/'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER

# 保存されたモデルの読み込み
gpt4_model = tf.keras.models.load_model('models/my_model.h5')

@app.route('/flask/upload', methods=['POST'])
def upload_file():
    if 'image' not in request.files:
        return "No file part", 400
    file = request.files['image']
    if file.filename == '':
        return "No selected file", 400

    filepath = os.path.join(app.config['UPLOAD_FOLDER'], file.filename)
    file.save(filepath)

    # 画像を前処理し、モデルに入力
    processed_image = process_image(filepath)
    result = call_gpt4(processed_image)

    # ファイルを削除
    os.remove(filepath)

    # JSON値を返却
    return jsonify(result)

def process_image(filepath):
    image = Image.open(filepath).convert('L')  # グレースケールに変換
    image = image.resize((28, 28))            # (28, 28) サイズにリサイズ
    image_array = np.array(image)
    image_array = image_array / 255.0          # 正規化
    image_array = image_array.flatten()        # フラットに変換
    image_array = np.expand_dims(image_array, axis=0)  # (1, 784) に変換
    return image_array

def call_gpt4(image_array):
    # モデルの予測を呼び出し
    predictions = gpt4_model.predict(image_array)
    response = {"predictions": predictions.tolist()}  # 必要に応じて変換
    return response

if __name__ == '__main__':
    app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024  # 最大16MBのファイルサイズを許可
    app.debug = True
    app.run()
```

※ソース冒頭で実行環境のPythonのパスを記述している（今回はTensorFlowの使用を行い、前途のリンクにもある理由があるため、Pythonのバージョンはこれである必要がある）

※必要なライブラリに関しては pip コマンドにてインストールすること。

## 雑感・あとがき

上記実行サーバソースに対して、

```
curl -X POST -F image=@166.jpg http://localhost:5000/flask/upload
```
のようなコマンド（送信ファイル名は任意）を送信したところの返却される値とは、小数点以下を多く持つ数値の配列である。
なので下記。

:::note info
画像を解釈可能なフォーマットにして、解析すること。機械がそれを行う起点となることは、特徴を数値に変換することである。今回はそこまでが取得できるだけの成果にとどまっている。
人工でない頭脳は、経験に基づき「画像が何であるのか」について、あらゆる角度から判定を行っている。
そのことに近付くことが、どれだけのステップを踏むことになるのか想像するのは容易でない。ただしそのスタート地点に立っている試みが始まっていることが確認できたことは、成果と言えると思っている。
:::

以上です。