K_DM Book
用語集
対象者 標準
対象者別の入口 →

対象者の設定はこのブラウザの中にだけ保存されます。

言語 JA

テキストの感情分析

05.3.1

テキストの感情分析

最終更新 2020-01-29 読了時間 4 分
まとめ
感情分析は、1 つの文を読んで「ポジティブか、ネガティブか」のラベルと、その確信度(0〜1 のスコア)を返す分類器です。このページでは HuggingFace の SiEBERT(siebert/sentiment-roberta-large-english)を pipeline("sentiment-analysis") で呼び出し、決算発表の英文 8 文を 1 文ずつ分類します。POSITIVE なら +score、NEGATIVE なら −score に符号を付けて signed_score 列にまとめると、文ごとの棒グラフ、全体の構成比、四半期ごとの平均スコアの推移が同じ数値から描けます。読み終えると、「決算のトーンが前四半期より弱まった」という印象を、平均スコア +0.28 → +0.51 のような数値で言えるようになります。

直感 #

決算発表の原稿を読むと、“Earnings were strong” のような自信のある文と、“We remain cautious” のような守りの文が混ざっています。人間が読めばトーンの違いは分かりますが、何十社・何四半期ぶんも読み比べるのは無理があります。感情分析モデルは、この「読んで受ける印象」を文ごとに POSITIVE / NEGATIVE のラベルと確信度に置き換える道具です。1 文ずつスコアを付けて平均すれば、原稿 1 本のトーンが 1 つの数値になり、四半期ごとに並べて比較できます。

詳細な解説 #

Hartmann, Jochen and Heitmann, Mark and Siebert, Christian and Schamp, Christina, “More than a feeling: Accuracy and Application of Sentiment Analysis”, International Journal of Research in Marketing(2022)

モデルのセットアップ #

HuggingFace上のsiebert/sentiment-roberta-large-englishを使用します。RoBERTa-large を 15 種類のデータセットで追加学習したモデルで、レビューやニュースなど幅広い英文で動きます。Google Colab上で使用する場合は事前にtransformersをインストールしてください。次のコードは、モデルのダウンロードと推論器の準備を 1 行で済ませます。

1
2
3
4
5
6
7
8
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from transformers import pipeline

sentiment_pipeline = pipeline(
    "sentiment-analysis", model="siebert/sentiment-roberta-large-english"
)

決算テキストの文単位分析 #

テキスト全体を「.」で区切り、一文ごとに感情分析を適用します。ここでは決算発表でよく使われる表現をサンプルとして使用します。原稿をまるごと 1 回で分類すると「全体として少しポジティブ」という 1 つの答えしか返りませんが、文に分ければ「売上の文は強気、コストの文は弱気」という内訳が見えます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
transcript = (
    "Revenue for the third quarter came in at 91 million, in line with previous guidance. "
    "Earnings were strong with net income reaching 47 million. "
    "However, operating costs increased significantly due to supply chain disruptions. "
    "We remain cautious about the near-term outlook given macroeconomic uncertainty. "
    "Our long-term growth strategy continues to deliver results. "
    "Cash flow generation was robust, enabling continued investment in key projects. "
    "We are concerned about rising interest rates impacting our debt servicing costs. "
    "The board has approved a dividend increase reflecting confidence in future earnings."
)

ts_list = [ts.strip() for ts in transcript.split(".") if len(ts.strip()) > 20]
scores = sentiment_pipeline(ts_list)

結果をDataFrameで整理 #

scores は辞書のリストなので、そのままでは並べ替えも集計もしにくいです。次のコードで文・ラベル・スコアを 1 つの表にまとめ、符号付きスコア signed_score を作ります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
results = pd.DataFrame({
    "text": ts_list,
    "label": [s["label"] for s in scores],
    "score": [round(s["score"], 4) for s in scores],
})
results["signed_score"] = results.apply(
    lambda row: row["score"] if row["label"] == "POSITIVE" else -row["score"],
    axis=1,
)
print(results[["label", "score", "text"]].to_string(index=False))

感情スコアの可視化 #

各文のスコアを棒グラフで描くと、テキスト内のトーンの推移が視覚的に把握できます。横軸が signed_score、縦軸が原稿の文順なので、「冒頭は強気で中盤に弱気の文が 2 つ挟まる」という流れが棒の向きの切り替わりとして見えます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fig, ax = plt.subplots(figsize=(10, 4))
colors = ["#10b981" if s > 0 else "#ef4444" for s in results["signed_score"]]
ax.barh(range(len(results)), results["signed_score"], color=colors)
ax.set_yticks(range(len(results)))
ax.set_yticklabels([t[:50] + "..." if len(t) > 50 else t for t in results["text"]], fontsize=8)
ax.set_xlabel("Sentiment Score(+: Positive, -: Negative)")
ax.set_title("文ごとの感情スコア")
ax.axvline(0, color="#64748b", linewidth=0.8)
ax.grid(axis="x", alpha=0.3)
fig.tight_layout()
plt.show()

テキスト全体のセンチメント集計 #

テキスト全体のポジティブ・ネガティブの割合と平均スコアを算出します。8 文のうち POSITIVE が 5 文なら構成比は 62%、signed_score の平均が +0.2 台なら「やや強気」という原稿全体の要約になります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
pos_count = (results["label"] == "POSITIVE").sum()
neg_count = (results["label"] == "NEGATIVE").sum()
avg_score = results["signed_score"].mean()

print(f"文数:     {len(results)}")
print(f"Positive: {pos_count} ({pos_count/len(results):.0%})")
print(f"Negative: {neg_count} ({neg_count/len(results):.0%})")
print(f"平均スコア: {avg_score:+.4f}")

# 円グラフで構成比を表示
fig, ax = plt.subplots(figsize=(5, 5))
ax.pie(
    [pos_count, neg_count],
    labels=["Positive", "Negative"],
    colors=["#10b981", "#ef4444"],
    autopct="%1.0f%%",
    startangle=90,
)
ax.set_title("センチメント構成比")
plt.show()

複数テキストの比較 #

複数の決算テキストを分析し、企業間や四半期間のセンチメント変化を比較するのが実務的な活用法です。次のコードは Q1・Q2・Q4 を仮の平均スコアで置き、上で計算した avg_score を Q3 として並べ、4 本の棒で推移を描きます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 各テキストの平均スコアを比較する例
quarterly_scores = {
    "Q1": 0.42,
    "Q2": 0.28,
    "Q3": avg_score,
    "Q4": 0.51,
}

fig, ax = plt.subplots(figsize=(7, 3.5))
bars = ax.bar(quarterly_scores.keys(), quarterly_scores.values(),
              color=["#10b981" if v > 0 else "#ef4444" for v in quarterly_scores.values()])
ax.set_ylabel("平均センチメントスコア")
ax.set_title("四半期ごとのセンチメント推移")
ax.axhline(0, color="#64748b", linewidth=0.8)
ax.grid(axis="y", alpha=0.3)
fig.tight_layout()
plt.show()

使いどころと落とし穴 #

SiEBERT は英語テキスト専用のモデルなので、日本語の決算短信にそのまま使うとほぼ全文が同じラベルに寄ります。日本語なら koheiduck/bert-japanese-finetuned-sentiment のような日本語で学習したモデルに model= を差し替えます。英文でも、金融の語彙に特化した FinBERT を使うと、“liability” や “exposure” のような一般の文脈では中立に見える語の扱いが決算文向きになり、SiEBERT より判定が安定します。

集計の単位も結果を左右します。文単位の signed_score を段落やセクション(業績概況・見通し・質疑応答)ごとに平均し直すと、決算発表のどの部分でトーンが変わったかが分かります。さらに四半期ごとの平均スコアと、発表翌日の株価リターンの相関を調べれば、テキストの感情が投資判断の材料になるかを自分のデータで検証できます。