!date
!python --version
Mon Apr 21 06:58:06 AM UTC 2025
Python 3.11.12
28. 極性分類システムの構築例(Bag-of-Wordsベース)#
28.1. 本演習の目標#
ルールベース(rule_based.ipynb)と機械学習との違いを理解する。
28.2. 実装方針#
特徴量を Bag-of-Words によるバイナリコーディング f(x) とする。学習器は、全ての単語に対する重み W による荷重和スコアを求め、スコアに基づき識別する。
特徴量関数
: bag-of-words
28.3. A. データセット用意(コピペ)#
!curl -O https://ie.u-ryukyu.ac.jp/~tnal/2025/dm/static/r_assesment_sentiment.xlsx
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 53514 100 53514 0 0 17962 0 0:00:02 0:00:02 --:--:-- 17957
import pandas as pd
filename = "r_assesment_sentiment.xlsx"
assesment_df = pd.read_excel(filename)
assesment_df.head()
title | grade | required | q_id | comment | wakati1 | wakati2 | sentiment | |
---|---|---|---|---|---|---|---|---|
0 | 工業数学Ⅰ | 1 | True | Q21 (1) | 特になし | 特に なし | 特に ない | 0 |
1 | 工業数学Ⅰ | 1 | True | Q21 (2) | 正直わかりずらい。むだに間があるし。 | 正直 わかり ず らい 。 むだ に 間 が ある し 。 | 正直 わかる ぬ らい 。 むだ に 間 が ある し 。 | -1 |
2 | 工業数学Ⅰ | 1 | True | Q21 (2) | 例題を取り入れて理解しやすくしてほしい。 | 例題 を 取り入れ て 理解 し やすく し て ほしい 。 | 例題 を 取り入れる て 理解 する やすい する て ほしい 。 | -1 |
3 | 工業数学Ⅰ | 1 | True | Q21 (2) | 特になし | 特に なし | 特に ない | 0 |
4 | 工業数学Ⅰ | 1 | True | Q21 (2) | スライドに書く文字をもう少しわかりやすくして欲しいです。 | スライド に 書く 文字 を もう少し わかり やすく し て 欲しい です 。 | スライド に 書く 文字 を もう少し わかる やすい する て 欲しい です 。 | -1 |
x_data = list(assesment_df['wakati1'])
y_data = list(assesment_df['sentiment'])
print(f"{x_data[0]=}, {type(x_data[0])=}")
print(f"{y_data[0]=}, {type(y_data[0])=}")
x_data[0]='特に なし', type(x_data[0])=<class 'str'>
y_data[0]=0, type(y_data[0])=<class 'int'>
# 学習用データ、テスト用データに分割
from sklearn.model_selection import train_test_split
# train_size = 学習用データの割合。
# random_state = 疑似乱数生成するためのシード値。
# シード値を固定しておくと「シャッフルするけど毎回同じシャッフル結果」を利用できる。
# 結果を再現できるため、動作確認や失敗分析をし易い。
# shuffle = シャッフするなら True。
X_train, X_test, y_train, y_test = train_test_split(x_data, y_data, train_size=0.8, random_state=1, shuffle=True)
print(f"{len(X_train)=}, {len(y_train)=}")
print(f"{len(X_test)=}, {len(y_test)=}")
print(f"{X_train[0]=}, {y_train[0]=}")
len(X_train)=136, len(y_train)=136
len(X_test)=34, len(y_test)=34
X_train[0]='python の 内容 は 予想 を 上回る ほど の 量 だっ た ので 、 まだ 理解度 が 完璧 と は 言え ない 状況 です 。 夏休み は 復習 を し て 、 2 学期 から また 新しい 言語 を 学ん で いき たい と 思い ます 。', y_train[0]=0
28.4. モジュール読み込み#
今回はどちらもカットして問題ないが、多くの実装で利用されるため使っています。
random: データセットをシャッフルするために利用。
tqdm: プログレス・バー(進捗状況)を表示するために利用。
import random
import tqdm
28.5. B. 特徴抽出(変更あり)#
Bag-of-Wordsにより特徴表現。
def extract_features(x: str) -> dict[str, float]:
features = {}
x_split = x.split(' ')
for x in x_split:
features[x] = features.get(x, 0) + 1.0
return features
# 実行例
text = X_train[8]
features = extract_features(text)
print(f"{text=}")
print(f"{features=}")
text='社会人 に 向け て の これから を 考える いい 機会 に なっ た ので よかっ た です 。'
features={'社会人': 1.0, 'に': 2.0, '向け': 1.0, 'て': 1.0, 'の': 1.0, 'これから': 1.0, 'を': 1.0, '考える': 1.0, 'いい': 1.0, '機会': 1.0, 'なっ': 1.0, 'た': 2.0, 'ので': 1.0, 'よかっ': 1.0, 'です': 1.0, '。': 1.0}
28.6. C. 分類器構築(少し修正)#
ルールベースでは、(1) good_words, bad_words, bias の3種類の重みを用意し、(2) それぞれの出現数に掛け合わせた総和によりスコアを求め、(3) しきい値処理により推定するという流れで処理した。
BoWベースでは、(1) 全ての単語に対して異なる重み(初期値0)を用意し、(2) それぞれの出現数に掛け合わせた総和によりスコアを求め、(3) しきい値処理により推定する。変更点は(1)のみ。
# 全ての重みを0に初期化
feature_weights = {}
def run_classifier(features: dict[str, float]) -> int:
'''入力された特徴辞書の極性を推定する。
入力 (features):特徴辞書。
出力1 (int): 推定ラベル: 良い評価(1)、悪い評価(-1)、どちらでもない(0)。
出力2 (score): 算出スコア。
'''
score = 0
for feat_name, feat_value in features.items():
score = score + feat_value * feature_weights.get(feat_name, 0)
if score > 0:
return 1, score
elif score < 0:
return -1, score
else:
return 0, score
for i in range(5):
print(f"{X_train[i]=}")
features = extract_features(X_train[i])
print(f"{features=}")
estimated_label, score = run_classifier(features)
true_label = y_train[i]
print(f"{score=}, {estimated_label=}, {true_label=}")
print("---")
X_train[i]='python の 内容 は 予想 を 上回る ほど の 量 だっ た ので 、 まだ 理解度 が 完璧 と は 言え ない 状況 です 。 夏休み は 復習 を し て 、 2 学期 から また 新しい 言語 を 学ん で いき たい と 思い ます 。'
features={'python': 1.0, 'の': 2.0, '内容': 1.0, 'は': 3.0, '予想': 1.0, 'を': 3.0, '上回る': 1.0, 'ほど': 1.0, '量': 1.0, 'だっ': 1.0, 'た': 1.0, 'ので': 1.0, '、': 2.0, 'まだ': 1.0, '理解度': 1.0, 'が': 1.0, '完璧': 1.0, 'と': 2.0, '言え': 1.0, 'ない': 1.0, '状況': 1.0, 'です': 1.0, '。': 2.0, '夏休み': 1.0, '復習': 1.0, 'し': 1.0, 'て': 1.0, '2': 1.0, '学期': 1.0, 'から': 1.0, 'また': 1.0, '新しい': 1.0, '言語': 1.0, '学ん': 1.0, 'で': 1.0, 'いき': 1.0, 'たい': 1.0, '思い': 1.0, 'ます': 1.0}
score=0.0, estimated_label=0, true_label=0
---
X_train[i]='特に なし'
features={'特に': 1.0, 'なし': 1.0}
score=0.0, estimated_label=0, true_label=0
---
X_train[i]='配布 資料 が 教科書 の 内容 に 沿っ て おり 、 わかり やすかっ た 。'
features={'配布': 1.0, '資料': 1.0, 'が': 1.0, '教科書': 1.0, 'の': 1.0, '内容': 1.0, 'に': 1.0, '沿っ': 1.0, 'て': 1.0, 'おり': 1.0, '、': 1.0, 'わかり': 1.0, 'やすかっ': 1.0, 'た': 1.0, '。': 1.0}
score=0.0, estimated_label=0, true_label=1
---
X_train[i]='Zoom の 音声 、 資料 画像 の 画質 など 特に 問題 なく 授業 を 受け られ た 。'
features={'Zoom': 1.0, 'の': 2.0, '音声': 1.0, '、': 1.0, '資料': 1.0, '画像': 1.0, '画質': 1.0, 'など': 1.0, '特に': 1.0, '問題': 1.0, 'なく': 1.0, '授業': 1.0, 'を': 1.0, '受け': 1.0, 'られ': 1.0, 'た': 1.0, '。': 1.0}
score=0.0, estimated_label=0, true_label=1
---
X_train[i]='たまに 説明 が ない コード が あっ たり し た ので 少し 戸惑っ た 。 いずれ はやっ て いく もの で は ある が 、 、 、'
features={'たまに': 1.0, '説明': 1.0, 'が': 3.0, 'ない': 1.0, 'コード': 1.0, 'あっ': 1.0, 'たり': 1.0, 'し': 1.0, 'た': 2.0, 'ので': 1.0, '少し': 1.0, '戸惑っ': 1.0, '。': 1.0, 'いずれ': 1.0, 'はやっ': 1.0, 'て': 1.0, 'いく': 1.0, 'もの': 1.0, 'で': 1.0, 'は': 1.0, 'ある': 1.0, '、': 3.0}
score=0.0, estimated_label=0, true_label=-1
---
28.7. New: 学習#
単語に対する重みを学習により求める。学習アルゴリズムは以下の通り。
もし推測結果が正しいなら、何もしない(重みを更新しない)。
もし推測結果が誤りなら、全ての特徴量を
新しい重み = 現在の重み + y * 特徴量
で更新する。case 1: 正解が1で、-1と誤った場合。
新しい重み = 現在の重み + 特徴量
特徴量が加算される。これにより重みがより正の方向に修正され、スコアが0より大きな値になりやすくなる。
case 2: 正解が-1で、1と誤った場合。
新しい重み = 現在の重み - 特徴量
特徴量が減算される。これにより重みがより負の方向に修正され、スコアが0より大きな値になりやすくなる。
補足
どちらでもない(0)に対する学習は行っていない(省略)。
# E. 性能評価関数(コピペ)
def calculate_accuracy(x_data: list[str], y_data: list[int]) -> float:
total_number = 0
correct_number = 0
for x, y in zip(x_data, y_data):
y_pred, score = run_classifier(extract_features(x))
total_number += 1
if y == y_pred:
correct_number += 1
return correct_number / float(total_number)
# 全ての重みを0に初期化
feature_weights = {}
# 学習前のスコア
train_accuracy = calculate_accuracy(X_train, y_train)
test_accuracy = calculate_accuracy(X_test, y_test)
print(f"before: {train_accuracy=:.5f}, {test_accuracy=:.5f}")
# 学習
NUM_EPOCHS = 10
for epoch in range(1, NUM_EPOCHS+1):
# データセットをシャッフル
data_ids = list(range(len(X_train)))
random.shuffle(data_ids)
# サンプルごとの処理
for data_id in tqdm.tqdm(data_ids, desc=f'Epoch {epoch}'):
x = X_train[data_id]
y = y_train[data_id]
if y == 0: # 「どちらでもない(0)」ケースはスキップ。
continue
# 予測
features = extract_features(x)
predicted_y, score = run_classifier(features)
# 予測結果が誤り時の重み更新処理
if predicted_y != y:
for feature in features:
feature_weights[feature] = feature_weights.get(feature, 0) + y * features[feature]
#print(f"{feature_weights=}")
train_accuracy = calculate_accuracy(X_train, y_train)
test_accuracy = calculate_accuracy(X_test, y_test)
print(f"{epoch=}: {train_accuracy=:.5f}, {test_accuracy=:.5f}")
before: train_accuracy=0.16912, test_accuracy=0.20588
Epoch 1: 100%|██████████| 136/136 [00:00<00:00, 31922.62it/s]
epoch=1: train_accuracy=0.78676, test_accuracy=0.52941
Epoch 2: 100%|██████████| 136/136 [00:00<00:00, 43168.26it/s]
epoch=2: train_accuracy=0.89706, test_accuracy=0.64706
Epoch 3: 100%|██████████| 136/136 [00:00<00:00, 7456.54it/s]
epoch=3: train_accuracy=0.80147, test_accuracy=0.67647
Epoch 4: 100%|██████████| 136/136 [00:00<00:00, 11287.06it/s]
epoch=4: train_accuracy=0.92647, test_accuracy=0.70588
Epoch 5: 100%|██████████| 136/136 [00:00<00:00, 15123.02it/s]
epoch=5: train_accuracy=0.94118, test_accuracy=0.67647
Epoch 6: 100%|██████████| 136/136 [00:00<00:00, 42011.00it/s]
epoch=6: train_accuracy=0.94118, test_accuracy=0.67647
Epoch 7: 100%|██████████| 136/136 [00:00<00:00, 14828.95it/s]
epoch=7: train_accuracy=0.88971, test_accuracy=0.73529
Epoch 8: 100%|██████████| 136/136 [00:00<00:00, 34317.49it/s]
epoch=8: train_accuracy=0.76471, test_accuracy=0.64706
Epoch 9: 100%|██████████| 136/136 [00:00<00:00, 41737.42it/s]
epoch=9: train_accuracy=0.81618, test_accuracy=0.67647
Epoch 10: 100%|██████████| 136/136 [00:00<00:00, 44749.77it/s]
epoch=10: train_accuracy=0.83088, test_accuracy=0.67647
28.8. D. 性能評価(コピペ)#
関数定義は既に定義済みなので、ここでは実行コードのみコピペ。
label_count = {}
for y in y_test:
if y not in label_count:
label_count[y] = 0
label_count[y] += 1
print(label_count)
train_accuracy = calculate_accuracy(X_train, y_train)
test_accuracy = calculate_accuracy(X_test, y_test)
print(f'Train accuracy: {train_accuracy}')
print(f'Dev/test accuracy: {test_accuracy}')
{-1: 9, 1: 18, 0: 7}
Train accuracy: 0.8308823529411765
Dev/test accuracy: 0.6764705882352942
28.9. E. 失敗分析(コピペ)#
def find_errors(x_data, y_data):
error_ids = []
y_preds = []
for i, (x, y) in enumerate(zip(x_data, y_data)):
pred, score = run_classifier(extract_features(x))
y_preds.append(pred)
if y != y_preds[-1]:
error_ids.append(i)
for _ in range(5):
my_id = random.choice(error_ids)
x, y, y_pred = x_data[my_id], y_data[my_id], y_preds[my_id]
print(f'{x}\ntrue label: {y}\npredicted label: {y_pred}\n')
find_errors(X_train, y_train)
特に なし
true label: 0
predicted label: 1
特に なし
true label: 0
predicted label: 1
特に なし
true label: 0
predicted label: 1
特に なし
true label: 0
predicted label: 1
特に なし
true label: 0
predicted label: 1