!date
!python --version
Mon Apr 21 06:26:17 AM UTC 2025
Python 3.11.12

27. 極性分類システムの構築例(ルールベース)#

27.1. 本演習の目標#

  • テキスト分類システムを構成する部品と全体像を把握する。

  • 実例を題材として分類精度を確認するとともに、失敗要因分析を通してシステム更新し、性能向上を試みる。

    • 主な更新箇所:特徴抽出、評価関数

  • 性能向上策が寄与したかどうかを検証するために、実験条件と評価結果を記録する。

27.2. 全体の流れ#

  • A. データセット用意

  • B. 特徴抽出

  • C. 分類器構築

  • D. 性能評価

  • E. 失敗分析

27.3. Tips#

本来ならば形態素解析を通してステミング(原型化)をした方がベターですが、今回は分かち書きのみに留めています。このため「良い」と「よい」「よかった」等は異なる単語として扱われます。興味のある人はmecabを用いた形態素解析を参照ください。今回は処理結果の例を後述のデータセットに含めてあります。分かち書き、形態素解析等の詳細は後日扱います。

27.4. データセット#

データは調査と解析班で収集された、知能除法コース専門科目の2021年度前期科目を対象に自由記述欄のデータです。これに當間が主観で「良い評価(1)、悪い評価(-1)、どちらでもない(0)」を sentiment 列に付与しました。これをExcelファイルとして用意したものが r_assesment_sentiment.xlsx です。このファイルを用意してから実行してください。(プログラムでは curl コマンドでダウンロードしています)

  • r_assesment_sentiment.xlsx

    • 列説明

      • title: 授業名

      • grade: 対象学年

      • required: 必修科目(True), 選択科目(False)

      • q_id: 問い番号

      • comment: 自由記述内容(原文)。

      • wakati1: commentをmecabで分かち書きしたテキスト。

      • wakati2: commentをmecabで分かち書きし、レマタイズしたテキスト。

      • sentiment: 極性ラベル。良い評価(1)、悪い評価(-1)、どちらでもない(0)。

27.5. 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  22086      0  0:00:02  0:00:02 --:--:-- 22094
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
# サンプル0のwakati1列とsentiment列を確認

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

27.6. B. 特徴抽出#

簡易的に特徴量関数を設計しています。本来なら次元毎に特徴量関数を用意し、次元毎の数値を定めて特徴ベクトルを設計します。しかしここでは簡易的な実装として以下のように処理しています。

  • step 1: 良い評価に用いられる単語群(good_words)、悪い評価に用いられる単語群(bad_words)を用意。

  • step 2: サンプル毎に good_words, bad_wordsの出現した回数をカウント。

  • step 3: それぞれの出現回数とバイアスを加えた3次元の特徴ベクトルを構築する。(実際には辞書として用意してるだけ)

    • 例えばgood_words1回、bad_words0回、バイアスを1(デフォルトでほんのり良いとする)とすると、{“good_word_count”: 1, “bad_word_count”: 0, “bias”:1} という辞書を構築する。ベクトル風に書くなら 1, 0, 1 の3次元の特徴ベクトルに相当する。

これらの設計は主観のため、good_words, bad_wordsをそれぞれどのように用意するか、出現時に+1カウントアップするだけで良いのか、バイアスをどう設計すべきか、そもそも3次元で良いのか、、、といったことは「ひとまずこの設計でやった」にすぎない。この結果が満足いかない場合には、失敗要因分析を通じて再設計し改善を目指すことになる。

def extract_features(x: str) -> dict[str, float]:
    '''特徴抽出部:テキストを受け取り、指定した特徴抽出結果を辞書型で返す。

    step 1: 句点(。)単位で処理するために文に分割する。
    step 2: good_words や bad_words の出現回数をカウント。ただし完全一致ではなく、部分一致。
    step 3: バイアスを設定。
    '''
    features = {}
    x_split = x.split(' ') # step 1: 句点区切りで文に分割。

    # step 2: 良い(悪い)評価に用いられる単語を列挙しておき、該当するたびにカウントする
    good_words = ['よかっ', 'でき']
    bad_words = ['ず', '難']
    for x_word in x_split: # step 2: 文単位で上記指定語が含まれるたびにカウントアップ
        if x_word in good_words:
            features['good_word_count'] = features.get('good_word_count', 0) + 1
        if x_word in bad_words:
            features['bad_word_count'] = features.get('bad_word_count', 0) + 1

    # step 3: デフォルトスコア用のバイアス値(固定)
    features['bias'] = 1

    return features

# 実行例
text = X_train[8]
features = extract_features(text)
print(f"{features=}: {text=}")
features={'good_word_count': 1, 'bias': 1}: text='社会人 に 向け て の これから を 考える いい 機会 に なっ た ので よかっ た です 。'

27.7. C. 分類器構築#

Bで設計した特徴ベクトルを元に「荷重和スコアが0を超えるなら1 (positive)、0未満なら-1 (negative)、それ以外なら0 (normal)」と判断する分類器を構築している。なお荷重和を求める工程では good_word_count, bad_word_count, bias それぞれ異なる重みを用意した上スコアを求めている。

特徴抽出同様に、重みをどのように設計するか、荷重和スコアを元にどのように判定するかといった細かな部分は「ひとまずこの設計でやった」だけである。必要に応じてチューニングすることになる。

# 良い(悪い)の単語に対する重み。バイアスにも重みを用意。
feature_weights = {'good_word_count': 1.0, 'bad_word_count': -1.0, 'bias': 0.5}

def run_classifier(x: str) -> tuple[int, float]:
    '''入力されたテキストの極性を推定する。

    入力 (x):テキスト
    出力1 (int): 推定ラベル: 良い評価(1)、悪い評価(-1)、どちらでもない(0)。
    出力2 (score): 算出スコア。
    '''
    score = 0
    for feat_name, feat_value in extract_features(x).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):
    estimated_label, score = run_classifier(X_train[i])
    true_label = y_train[i]
    print(f"{score=}, {estimated_label=}, {true_label=}, {X_train[i]=}")
score=0.5, estimated_label=1, true_label=0, X_train[i]='python の 内容 は 予想 を 上回る ほど の 量 だっ た ので 、 まだ 理解度 が 完璧 と は 言え ない 状況 です 。 夏休み は 復習 を し て 、 2 学期 から また 新しい 言語 を 学ん で いき たい と 思い ます 。'
score=0.5, estimated_label=1, true_label=0, X_train[i]='特に なし'
score=0.5, estimated_label=1, true_label=1, X_train[i]='配布 資料 が 教科書 の 内容 に 沿っ て おり 、 わかり やすかっ た 。'
score=0.5, estimated_label=1, true_label=1, X_train[i]='Zoom の 音声 、 資料 画像 の 画質 など 特に 問題 なく 授業 を 受け られ た 。'
score=0.5, estimated_label=1, true_label=-1, X_train[i]='たまに 説明 が ない コード が あっ たり し た ので 少し 戸惑っ た 。 いずれ はやっ て いく もの で は ある が 、 、 、'

27.8. D. 性能評価#

今回のモデルは学習を行わず、ルールベースで構築したシンプルなモデルである。そのためテストデータだけではなく学習データに対する評価も行っている。

def calculate_accuracy(x_data: list[str], y_data: list[int]) -> float:
    '''推定結果の一致率を返す。

    入力1 (x_data): テキスト群。
    入職2 (y_data): 正解ラベル群。
    出力 (float): 一致率。
    '''
    total_number = 0
    correct_number = 0
    for x, y in zip(x_data, y_data):
        y_pred, score = run_classifier(x)
        total_number += 1
        if y == y_pred:
            correct_number += 1
    return correct_number / float(total_number)

train_accuracy = calculate_accuracy(X_train, y_train) # 学習データに対する正解率
test_accuracy = calculate_accuracy(X_test, y_test)    # テストデータに対する正解率
print(f'Train accuracy: {train_accuracy:0.3f}')
print(f'Test accuracy: {test_accuracy:0.3f}')
Train accuracy: 0.493
Test accuracy: 0.529

27.9. E. 失敗分析#

学習データにおける失敗事例を観察し、性能向上のためにどのような工夫が行えそうか検討してみよう。

  • システム改善に向けて要因分析をする際に、テストデータを用いてはいけません。それは リーク(Data Leakage) になります。

  • なお以下では「失敗した事例をランダムに5件選ぶ」形で出力しています。一般的には全ての失敗事例を眺めるのは大変だからです。

import random
def find_errors(x_data, y_data, num=5):
    '''num件の失敗事例を出力する。
    step 1: 指定されたデータセットに対する失敗事例ID集合 (error_ids) を作成。
    step 2: error_idsからランダムに num 件選び、出力する。

    NOTE: 乱数で選ばれる点に注意。
    '''

    # step 1: 指定されたデータセットに対する失敗事例ID集合 (error_ids) を作成。
    error_ids = []
    y_preds = []
    for i, (x, y) in enumerate(zip(x_data, y_data)):
        pred, score = run_classifier(x)
        y_preds.append(pred)
        if y != y_preds[-1]:
            error_ids.append(i)

    # step 2: error_idsからランダムに num 件選び、出力する。
    for _ in range(num):
        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: -1
predicted label: 1

python の 内容 は 予想 を 上回る ほど の 量 だっ た ので 、 まだ 理解度 が 完璧 と は 言え ない 状況 です 。 夏休み は 復習 を し て 、 2 学期 から また 新しい 言語 を 学ん で いき たい と 思い ます 。
true label: 0
predicted label: 1

中間 ・ 期末 の レポート など の 提出 を メール で 行う の は 良い が 、 出来れ ば 受理 さ れ た か どう か の 確認 が できる 形 に し て いただき たい ( 成績評価 において 提出 物 の 割合 が 大きい ため 、 正しく 受理 さ れ た か どう か が 気 に なる )
true label: -1
predicted label: 1

課題 の 点数 を 早め に 公開 し て もらえる と よかっ た と 思い ます 。 
 また 、 試験 に関して 疑問 点 を メール に 送っ て い た と 思い ます が 、 それ に 回答 し て いただける と よかっ た と 思い ます 。
true label: -1
predicted label: 1

特に なし
true label: 0
predicted label: 1

27.10. 難しいケース#

「これで良い」という万能な解決策はなく、状況に応じて「ベターな意思決定」をすることになる。

27.10.1. 難しいケース1: 低出現語 (low frequency words)#

  • 状況: コーパス内にそもそも殆ど出現していない。

  • 対応策?

    • 全ての失敗事例についてシステム更新し続ける?

    • 他資源(例えば商品レビュー)を用いて出現回数を増加させる?

from collections import Counter

comments = assesment_df['wakati1']
all_comments = ' '.join(comments)
splited_comments = all_comments.split(' ')
counts = Counter(splited_comments)
counts.most_common(5)
[('た', 329), ('が', 259), ('の', 248), ('。', 229), ('て', 201)]
# 低出現語
counts.most_common()[::-1][:20]
[('!', 1),
 ('つい', 1),
 ('試し', 1),
 ('フレームワーク', 1),
 ('いろんな', 1),
 ('もらっ', 1),
 ('コーディング', 1),
 ('GitHub', 1),
 ('チーム', 1),
 ('アプリケーション', 1),
 ('JavaScript', 1),
 ('CSS', 1),
 ('/', 1),
 ('HTML', 1),
 ('モバイルアプリ', 1),
 ('ほう', 1),
 ('会', 1),
 ('報告', 1),
 ('どちら', 1),
 ('焼き', 1)]

27.10.2. 難しいケース2: 活用する語#

  • 状況: 動詞、形容詞、形容動詞、助動詞といった「活用により変化する語」のため、ルール追加しても漏れが出てしまう。

  • 対応策?

    • 原型を利用する?

      • 言語ごとに固有の形態素解析が必要。

      • 参考: 日本語の代表的な形態素解析器: 2019年末版 形態素解析器の比較

      • 精度は100%ではないし、目的によっては「自分にとっては不適切な処理」をすることもあることに注意。

for word in comments:
    if '難し' in word:
        print(word)
まだ 1 年 次 という こと も あり 、 特に 難しく なく て よかっ た です 。
難しかっ た です
この 講義 の おかげ で 、 周り の 人 と 話す こと が でき た ので とても よかっ た です 。 この 講義 が なけれ ば 私 は 前期 の 間 で 友達 を 作る こと は 難しかっ た の で は ない か と 思い ます 。 また 、 企業 人 インタビュー や 履修 計画 表 作成 を通して 自分 の 将来 を 見通す こと が でき た の は とても 大きい です 。 この よう な 機会 を 設け て くださ り ありがとう ござい まし た 。
プログラミング を 全く 触っ た こと が なかっ た ので とても 難しかっ た 。
課題 は 学び はじめ にとって は 難しかっ た けど 達成感 は GOOD
比較的 人気 で 修得 が 簡単 な python で すら この 難し さ なら 、 先 が 思いやら れ ます 。
課題 について は 解答 と 解説 が しばらく 明示 さ れ ず 、 解答 自体 は 試験 の 1 , 2週間 ほど 前 に 公開 さ れ た ものの 解説 が ない ため 理解 が し づらかっ た です 。 
 試験 について は 、 特に 期末試験 について な の です が 、 大半 の 人 が 勉強 でき て い ない で あろ う 分野 の 問題 が 出さ れ た という こと と 問題 の 量 が それなり に 多かっ た ため に 非常 に 難しく 感じ まし た 。 実際 の クラス 平均 点 も 低かっ た です 。 
 また 、 中間試験 と 期末試験 を 2週 連続 で 実施 する の は 来年以降 の 授業 で は やめ て いただき たい です 。 どうしても 対面 で 実施 でき ない の なら すぐ に オンライン に 切り替え て 中間試験 の 期間内 に 実施 する べき だ と 思い ます 。 大変 でし た 。 
 最後 に 課題 と 試験 に 評価 方法 な の です が 、 webclass で の 完全 解答 型 の ため 採点 が 少し 厳しい と 感じ まし た 。 そこ も できれ ば 何らかの 形 で 改善 し て いただける と いい と 思い ます 。
・ 課題 の 点数 が 、 この 講義 が 全て 終わっ た のに 未だ すべて 採点 さ れ て い ない 点 が 不服 。 受講 者 が 多い だ とか 、 採点 する 側 も 忙しい だ と か 色々 ある と 思い ます が 、 そもそも 採点 する 側 が 捌き きれ ない ほど 課題 を 出し て いる という 現状 は 適切 でしょ う か 。 
 ・ 自身 の 努力 不足 かも しれ ませ ん が 、 率直 に 言っ て 試験 の 難易度 が 難しい 。 課題 で 解い た 問題 と 比べ て も 難易度 が 数 段 違う と 思い ます 。 この 授業 で は 「 配布 資料 」 「 課題 」 「 普段 の 講義 」 が 主 に 提供 さ れ まし た が 、 それら を 用い て 勉強 を し 、 また 足り ない と 思っ た ところ を 自分 で 調べ て 学習 し て も 、 試験 で 思う よう な 点 が 取れ ませ ん でし た 。 なので 試験 難易度 の 高さ に は 不満 が 残り ます 。 
 ・ 評価 方法 の 課題 20% 、 テスト 40 + 40 = 80% は 他 の 授業 で も よく 見 られる 配分 な ので 、 それ 自体 に は 不満 は あり ませ ん 。 しかし 、 先 ほど 書い た 通り 、 課題 が 採点 さ れ ない 点 や 、 試験 の 難易度 に 不満 が ある ため 、 結果的 に この 配分 も 良く ない の で は ない か 、 と 思い ます 。
試験 内容 に 初めて 見る 問題 が あっ た . さらに その後 の 解説 も あまり なかっ た ので 難易度 が 他 の 授業 に 比べ て 高 すぎる と 感じ まし た . それ は , 内容 が 難しい と いう より も 授業 で 試験 に 出る 内容 を 網羅 し 切れ て い ない こと による もの と 思い まし た . 遠隔 の 試験 で あっ た こと が 大きい という こと は 重々 承知 の 上 で , 問題 内容 が 伝わり 辛かっ た ので 他 の 授業 の よう に どうにか 対応 し て 欲しかっ た です . また , 平均 点 が 他 の 授業 と 比べ て 低 すぎる ので 追加 で 課題 など が 欲しかっ た です .
講義 全体 の 感想 として は 、 毎年 単位 を 落とす 人 が 多い という の も 納得 できる 難し さ だっ た と 思い ます 。 生徒 にとって 不親切 な 点 が 多く 、 本当に 前年度 まで の 生徒 から の 要望 は ちゃんと 取り入れ られ て いる の だろ う か と 疑問 に 思い まし た 。 
 講義 の 難易度 を 簡単 に し て ほしい という わけ で は なく 、 今後 少し でも 生徒 に 親切 な 授業 に し て もらえれ ば 単位 も 取り やすく なる の か な という 風 に 感じ まし た 。 
 講義 で 学ん だ 内容 自体 は とても 良かっ た です 。
・ この 授業 は 単位 を 落とす 学生 が 多い と 聞い た が 、 その 通り だ と 思う 。 先生 の 説明 が 、 予習 を し て い て も わかり づらい し 、 課題 ・ 試験 の 内容 も 難しい 。 
 ・ 課題 の 採点 が まだ さ れ て い ない 。 忙しい の は 学生 皆 承知 し て いる が 、 5月 出し た 課題 で すら 採点 さ れ て い ない の は いかが な もの か と 思う 。
朝 から 2 時間 連続 で 非常 に 大変 で 、 内容 も 難しかっ た が 、 先生 の 教え 方 も 人柄 も 良かっ た おかげ で 、 最後 まで 乗り越え られ た と 思う 。
講義 を通して 、 確率統計 に関する 知識 を 獲得 する こと が でき た と 思い ます 。 これら の 知識 を 利用 し て 個人的 に 実験 し て み たい こと も 出来 た ので 、 講義 を 受け て 良かっ た なぁ と しみじみ 思い ます 。 
 自分 は 数学 は 普通 〜 やや 得意 な 方 です が 、 難易度 は 「 難しい が 勉強 すれ ば 必要 な 知識 を 獲得 できる 」 という ちょうど 良い 難易度 でし た 。 先生 の 説明 も 分かり やすく 、 また 、 たびたび 実際 に これら の 知識 を どう 使う の か 、 という 実践 的 な 面 で も お話 が 聞け て とても 面白かっ た です 。 
 ありがとう ござい まし た
講義 中 は 内容 が 難しく 感じ て い た が 、 後日 見返し たり する と 理解 でき た こと が よく あっ た 。 
 今後 の 研究 など を 踏まえ た 講義 を し て くれ た ので 、 ため に なる こと が 多かっ た 。
課題 が 難しい もの が 多く 、 時間 を 多く とっ て もらえ た の は 非常 に 良かっ た です が かなり きつかっ た です 。 ありがとう ござい まし た 。

27.10.3. 難しいケース3:否定#

  • 状況:否定語により極性が反転することがある。

    • まだ という こと あり 特に 難しく なく よかっ です

  • 対応策?

27.10.4. 難しいケース4: 暗喩、例え#

  • 思わずクソゲーと叫びたくなるような難易度

    • 「難」という単語があるのでそれだけで negative 判断しやすいが、どのぐらい negative かという度合いを図ろうとすると難しい。

  • 他の例

    • 猫に小判, 花より男子

    • it's a piece of cake

    • オノマトペ

      • さらさらとした髪

  • 対応策?

    • ???