Python, Go, TypeScriptによるAIアプリ開発(1)

## はじめに
私の会社がAIのアプリ開発を主軸にやっている会社なのですが、自分がAIやデータの知識がヨワヨワなことに気づきました。今後プロジェクトに参画する予定なので、AIのアプリを開発しながらAIとWeb開発を同時に学んでしまおうという意図で取り組みました。
今回の技術スタックは以下の通りです。
- **フロントエンド**: TypeScript  
- **バックエンド**: Go + Python

### なぜバックエンドで2つの言語を使うのか
フロントエンドでTypeScriptを使うのは当たり前になってきていると思いますが、今回はバックエンドで2つの言語を採用しております。
意図としては以下のようなものがあります。

- **Python**: AIモデル構築に最適  
- **Go**: 軽量で高速なAPIサーバー構築に最適  
- **役割分担**によるメンテナンス性と柔軟性の向上  
- 実際の巨大なサービスでは複数言語を併用していることが多い  

用はそれぞれの言語の得意な部分を生かして開発できるということです。


## 開発の全体像
1. **AIモデル構築**（今回はここ！）  
2. **バックエンド・フロントエンド開発**  
3. **修正・機能追加** 

この記事では、ステップ1の**AIモデル構築**に焦点を当てて進めます。2～3か月以内の完成を目標にしています。


## AIモデル構築
### データセットの説明
今回はkaggleの「Emotion」データセットを使用しました。
-**URL**:https://www.kaggle.com/datasets/nelgiriyewithana/emotions/data
-**内容**:英語のTwitterのメッセージに感情ラベル（全6種）が付与されています。
以下翻訳したデータセットの説明：

「Emotions」データセットへようこそ。このデータセットは、英語のTwitterメッセージを6つの基本的な感情（怒り、恐れ、喜び、愛、悲しみ、驚き）で丁寧にアノテーションしたコレクションです。短文形式のソーシャルメディア上で表現される多様な感情スペクトラムを理解し、分析するための貴重なリソースとなっています。

本データセットの各エントリは、Twitterメッセージを表すテキストセグメントと、そこに含まれる主要な感情を示すラベルで構成されています。感情は以下の6つのカテゴリに分類されています：

悲しみ (0)
喜び (1)
愛 (2)
怒り (3)
恐れ (4)
驚き (5)
感情分析、感情分類、またはテキストマイニングに興味がある方にとって、このデータセットは、ソーシャルメディアの感情的な風景を探求するための豊かな基盤を提供します。




とのことです。
もう少し説明を付けくわえると以下のような3つのカラムで構成されております。つまり今回構築するAIモデルはテキストから感情を分析するというものになります。

| カラム名 | 説明                                         |
|----------|--------------------------------------------|
| `id`     | データの一意識別子                          |
| `text`   | ツイートの内容（英語）                      |
| `label`  | ツイートが持つ主要な感情（以下の6カテゴリ）  |

それでは実際にAIモデルを構築していきます。

### Google ColabでのAIモデル構築
今回Google Colabを使ってAIモデルを構築していきますが、念のためGoogle Colabについて解説します。
Google ColabはPythonや機械学習の環境を提供しているツールで、環境構築なしにすぐプログラミングが行えます。普通にGoogle Colabとかで検索すれば出てくるので、小難しいダウンロードなどはいりません。
Google Colab上でAIモデルを構築しそのモデルをダウンロードしてアプリ開発をローカルで行うという流れです。

ではさっそくコードの解説に移ります。


### 必要なライブラリのインポート
まずは以下のように必要なライブラリをインポートします。
```
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
from sklearn.metrics import classification_report
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, LSTM, Embedding, Dropout
from tensorflow.keras.optimizers import Adam
import pickle
import matplotlib.pyplot as plt
import kagglehub
import os
```

一旦ライブラリの説明は省きます。後々使ったときに解説していきます。


### データセットの読み込み
次に今回使うkaggleのデータセットを読み込んでいきます。
```
path = kagglehub.dataset_download("nelgiriyewithana/emotions")
csv_file_path = os.path.join(path, 'text.csv')
data = pd.read_csv(csv_file_path)

# 欠損値の確認と削除
data = data.dropna()
```
kaggeleのデータセットを読みこみ、data=data.dropna()でデータに欠損値があればNaを変わりに入れるという処理を行っています。（恐らく今回は欠損値はなかったはず）

### データの前処理
ここでデータを機械学習モデルが扱いやすい形式に変換します。

```
# テキストとラベルの取得
texts = data['text'].values
labels = data['label'].values
# テキストのトークン化
tokenizer = Tokenizer(num_words=5000)
tokenizer.fit_on_texts(texts)
X = tokenizer.texts_to_sequences(texts)
# シーケンスの長さを揃える
X = pad_sequences(X, maxlen=100)
# ラベルエンコーダーを使用してラベルを数値に変換
label_encoder = LabelEncoder()
y = label_encoder.fit_transform(labels)
```
主にやっていることはテキストを数値データに変換して、機械学習モデルが処理できる形にしています。

- tokenizer = Tokenizer(num_words=5000)で頻出単語上位5000語のみを対象にトークン化を行います。（計算コスト削減のため）

- tokenizer.fit_on_texts(texts)でtextを分析し、各単語に固有のインデックス（数値）を割り当てます。
例えば: happy->1, sad->2など

- X=tokenizer.texts_to_sequences(texts)で各テキストを、単語のインデックスに基づいたシーケンス（数値のリスト）に変換します。
例えば： I feel happy->[10, 45, 1]など

- X = pad_sequences(X, maxlen=100)で各テキストのシーケンスを一定の長さで揃えます。（モデルは入力データの形が揃っている必要があるため）
例えば：[1,2,3]->[0,0,..., 1,2,3] ※長さが100に満たない場合、先頭を0で埋めます。

- label_encoder = LabelEncoder(),
  y = label_encoder.fit_transform(labels)
そして数値でラベル付けをします。今回の場合はデータセットがもともと数値ラベル化されていたので必要ない処理ですが、多くのテキストデータではこの処理が必要なので書いてます。


### モデル構築
それではモデルを構築していきたいと思います。今回はLSTMというアルゴリズムを用いて分類モデルを構築します。
LSTMは時間的な依存関係を持つデータに強いニューラルネットワークで、テキスト音声など、順序が重要なデータに適しています。今回のようなテキストデータの場合、LSTMは過去の単語や文脈を考慮して現在の単語を予測する能力を持っています。


```
# モデルの定義
model = Sequential()

# 埋め込み層
model.add(Embedding(input_dim=5000, output_dim=128, input_length=100))

# LSTM層
model.add(LSTM(128, return_sequences=False))

# ドロップアウト層
model.add(Dropout(0.5))

# 出力層
model.add(Dense(6, activation='softmax'))  # 6クラスの分類

# モデルのコンパイル
model.compile(loss='sparse_categorical_crossentropy', optimizer=Adam(), metrics=['accuracy'])
```
- model = Sequential()はKerasのモデル構築のための基本的なクラスで、ニューラルネットワークの層を順番に重ねていく方法にしてます。

- model.add(Embedding(input_dim=5000, output_dim=128, input_length=100))
この層では各単語を128次元のベクトルに変換し、単語間の意味的な関係を捉える働きをしています。
input_dim=5000：単語の語彙数で、テキストデータに登場する単語数の上限を5000に設定します。
output_dim=128: 各単語の埋め込みベクトルの次元数を示します。
input_length=100: 各入力テキスト（シーケンス）の長さを100に固定。

- model.add(LSTM(128, return_sequences=False))
LSTM層は、時系列データやシーケンスデータの長期的な依存関係を学習するために使用され、特に勾配消失問題にLSTMは優れており、長期間の情報の保持が可能です。

- model.add(Dropout(0.5))
ドロップアウトは通常、過学習を防ぐために使用され、Dropout(0.5)とすることでニューロンの50%をランダムに無効にします。

- model.add(Dense(6, activation='softmax'))
出力層ではSoftmax関数を使用し、感情分類のために6つのクラスに分類します。

- model.compile(loss='sparse_categorical_crossentropy', optimizer=Adam(), metrics=['accuracy'])
最後にモデルを訓練する準備をします。
loss='sparse_categorical_crossentropy'で損失関数でクロスエントロピー損失を使用。
optimizer=Adam()で学習率を自動的に調整。
metrics=['accuracy']でモデルの評価指標として精度を使用。


### モデルの訓練と評価
次にデータを分割して訓練を行い、評価と予測を行う一連の処理を行います。

```
# モデルのビルド
model.build(input_shape=(None, 100))
model.summary()

# データの分割
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# モデルの訓練
history = model.fit(X_train, y_train, epochs=5, batch_size=64, validation_data=(X_test, y_test))

# 評価
loss, accuracy = model.evaluate(X_test, y_test, verbose=1)
print(f"Test Loss: {loss}")
print(f"Test Accuracy: {accuracy}")

# 予測
y_pred_prob = model.predict(X_test)
y_pred = np.argmax(y_pred_prob, axis=1)

# クラス名を手動で指定
class_names = ['sadness', 'joy', 'love', 'anger', 'fear', 'surprise']

# 精度レポート
print(classification_report(y_test, y_pred, target_names=class_names))
```
- model.build(input_shape=(None, 100))
  model.summary()
ここでモデルの構造を表示しています。今回構築したモデルは以下のようになっているはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3847760/fbf07e89-f482-ffe5-aee7-104ebfc0e07c.png)

行の説明：
Embedding Layer：テキストデータをベクトル表現に変換する層
LSTM Layer：LSTMアルゴリズムを行う層。シーケンスデータの文脈情報を学習し、長期依存性（過去の情報）を保持しながら、現在の入力を処理する。
Drpout Layer：過学習を防ぐための層。訓練時にランダムでニューロンを無効化します。
Dense Layer：最終的な予測を行う層。全結合層として動作し、入力データを線形変換します。

列の説明：
Layer(type)：各レイヤーの種類
Output Shape：レイヤーを通過した後の出力データを表示します。例えば(None, 100, 128)はバッチサイズが任意（None）で出力のシーケンス長が100, 各単語が128次元のベクトルであることを示します。
Param #：そのレイヤーのトレーニング可能なパラメータの数（重みやバイアス）を示します。


- #データの分割
  X_train, X_test, y_train, y_test = train_test_split(X, y, 
  test_size=0.2, random_state=42)
データの20%をテストデータとして使用し、80%をトレーニングデータとして使用します。random_state=42で分割のランダム性を固定しています。同じランダム状態を指定すると実行するたびに同じ分割結果が得られます。


- #モデルの訓練
  history = model.fit(X_train, y_train, epochs=5, batch_size=64, 
  validation_data=(X_test, y_test))
エポック数（全データセットを何回学習するか）を5、バッチサイズ（データを分割して処理する単位）を64にしてモデルの最適化を行います。


- #評価
  loss, accuracy = model.evaluate(X_test, y_test, verbose=1)
  print(f"Test Loss: {loss}")
  print(f"Test Accuracy: {accuracy}")
学習済みモデルがテストデータに対してどの程度の損失（loss）と精度（accuracy）を持つかを計測し、それらの結果を出力しています。

  
- #予測
  y_pred_prob = model.predict(X_test)
  y_pred = np.argmax(y_pred_prob, axis=1)
model.predict(X_test)でテストデータに対してモデルが予測を行い、各サンプルがクラスに属する確率を出力します。
例えば、6つのクラスの場合1つのサンプルに対して以下のような形で出力されます。
[0.1, 005, 0.7, 0.1, 0.03, 0.02]

np.argmax(y_pred_prob, axis=1)で最も確率が高いクラスのインデックスを取得します。


- #クラス名を手動で指定
  class_names = ['sadness', 'joy', 'love', 'anger', 'fear', 'surprise']
モデルの出力は数値インデックスなので、数値インデックスを感情名で変換しています。


- #精度レポート
  print(classification_report(y_test, y_pred, 
  target_names=class_names))

精度レポートでは、モデルの予測性能を評価するための指標を詳細に示しています。以下のような結果が出ているはずです。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3847760/e040e522-1010-3c7e-ac2b-84faeb4c5a8d.png)

sadnessを例に説明します。
Precision（適合率）は正と予測されたもののうち、実際に正である割合です。例えばsadnessに対して0.98の精度は、sadnessと予測されたもののうち、98%が実際にsadnessであることを意味します。

Recall（再現率）は実際に正であるもののうち、モデルが正しく予測できた割合です。sadnessの場合は実際にsadnessでるデータの97%がsadnessとして正しく予測されたことを意味します。

F1-scoreは精度と再現率の調和平均です。
F1=2*(Precision*Recall/Precision+Recall)
sanessに対してF1=0.97ということは、精度と再現率のバランスがいいことを示します。

Supportは各クラスの実際のサンプル数です。sadnessは24,201個のサンプルがあるということを示します。


### グラフの描画
最後にモデルの訓練中に記録された損失（loss）と精度（accuracy）の推移を可視化するグラフをプロットします。コードと実行結果はそれぞれ以下のようになるはずです。

```
# 損失と精度のグラフ化
plt.plot(history.history['accuracy'], label='accuracy')
plt.plot(history.history['val_accuracy'], label='val_accuracy')
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.legend(loc='best')
plt.show()

plt.plot(history.history['loss'], label='loss')
plt.plot(history.history['val_loss'], label='val_loss')
plt.title('Model Loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend(loc='best')
plt.show()
```

実行結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3847760/1d469f73-7a91-02f1-bd8e-e54d8cb61550.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3847760/b07faf4f-e93a-affc-90e7-fdee2ade5755.png)

エポック数ごとの訓練精度と検証精度を見てみると、どちらも最終的には95%近くになっており、訓練データと検証データの精度は近いので過学習も発生していないと考えられます。

訓練損失と検証損失においてもどちらも減少傾向にあり、最終的に非常に小さな値に帰着しているのでこちらも問題はないと思われます。


## 最後に
データセットが良かったせいもあると思いますが、比較的よいAIモデルが構築できたのではないでしょうか。構築したAIモデルを生かしどのようにアプリに組み込んでいくのかを次回以降やって言いたいと思います。